Skip to content

Instantly share code, notes, and snippets.

@thiezn
Last active November 14, 2023 12:37
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save thiezn/eeb78dcdc3902cdb2f33f9050d6d429d to your computer and use it in GitHub Desktop.
Save thiezn/eeb78dcdc3902cdb2f33f9050d6d429d to your computer and use it in GitHub Desktop.
HackerOne API Program and scope retrieval
#!/usr/bin/env python3
"""Interact with HackerOne Hacker API.
First generate an API token through the Hackerone website and initialize the class:
>>> username = "YOUR_USER_NAME"
>>> token = "GENERATE_AN_API_TOKEN_THROUGH_HACKERONE_WEBSITE"
>>> session = HackerOneSession(username, token)
To retrieve a single program
>>> hackerone_program = session.get_program("security")
>>> print(hackerone_program)
<HackerOneProgram HackerOne, 16 assets>
>>> print(hackerone_program.program.offers_bounties)
True
To list all programs run the following. Note it will take some time to retrieve
all programs. Please be concious of your fellow hackers and limit the amount of
API calls you make.
Note that when listing programs, the assets won't be returned. Use the get_program() or
get_assets() API call for this.
>>> all_programs = session.list_programs()
>>> for program in all_programs:
>>> print(program)
<HackerOneProgram Node.js third-party modules, 0 assets>
<HackerOneProgram Internet Freedom (IBB), 0 assets>
...truncated output...
>>> for asset in session.list_assets(all_programs[0]):
>>> print(asset)
<HackerOneAsset URL api.example.com>
"""
import requests
from dataclasses import dataclass
from datetime import datetime
from typing import Optional, Set
from enum import Enum
class HackerOneAssetType(Enum):
"""Class representing known types in HackerOne assets."""
URL = "URL"
OTHER = "OTHER"
GOOGLE_PLAY_APP_ID = "GOOGLE_PLAY_APP_ID"
APPLE_STORE_APP_ID = "APPLE_STORE_APP_ID"
WINDOWS_APP_STORE_APP_ID = "WINDOWS_APP_STORE_APP_ID"
CIDR = "CIDR"
SOURCE_CODE = "SOURCE_CODE"
DOWNLOADABLE_EXECUTABLES = "DOWNLOADABLE_EXECUTABLES"
HARDWARE = "HARDWARE"
OTHER_APK = "OTHER_APK"
OTHER_IPA = "OTHER_IPA"
TESTFLIGHT = "TESTFLIGHT"
@dataclass
class HackerOneAsset:
"""Class representing an asset of a HackerOne Program."""
id: str
type: HackerOneAssetType
identifier: str
eligible_for_bounty: bool
eligible_for_submission: bool
max_severity: str
created_at: datetime
updated_at: datetime
instuction: Optional[str]
reference: Optional[str]
confidentiality_requirement: Optional[str]
integrity_requirement: Optional[str]
availability_requirement: Optional[str]
def __repr__(self) -> str:
"""Pretty representation of class instance."""
return f"<HackerOneAsset {self.type} {len(self.identifier)}>"
@classmethod
def load_from_dict(cls, asset_dict: dict):
"""Initialize class instance from Dictionary object."""
return cls(
asset_dict["id"],
HackerOneAssetType(asset_dict["attributes"]["asset_type"]),
asset_dict["attributes"]["asset_identifier"],
asset_dict["attributes"]["eligible_for_bounty"],
asset_dict["attributes"]["eligible_for_submission"],
asset_dict["attributes"]["max_severity"],
datetime.fromisoformat(asset_dict["attributes"]["created_at"].rstrip("Z")),
datetime.fromisoformat(asset_dict["attributes"]["updated_at"].rstrip("Z")),
asset_dict["attributes"].get("instruction"),
asset_dict["attributes"].get("reference"),
asset_dict["attributes"].get("confidentiality_requirement"),
asset_dict["attributes"].get("integrity_requirement"),
asset_dict["attributes"].get("availability_requirement"),
)
def __hash__(self):
"""Allow for use in Python Sets."""
return hash(self.id)
def __eq__(self, other):
"""Compare two class instances."""
if other.id == self.id:
return True
return False
@dataclass
class HackerOneProgram:
"""Class representing a single HackerOne Program."""
id: str
# Program attributes
handle: str
name: str
currency: str
profile_picture: str
submission_state: str
triage_active: str
state: str
started_accepting_at: datetime
number_of_reports_for_user: int
number_of_valid_reports_for_user: int
bounty_earned_for_user: float
last_invitation_accepted_at_for_user: Optional[str]
bookmarked: bool
allows_bounty_splitting: bool
offers_bounties: bool
# Assets
assets: Set[HackerOneAsset]
def __repr__(self) -> str:
"""Pretty representation of class instance."""
return f"<HackerOneProgram {self.name}, {len(self.assets)} assets>"
@property
def program_url(self) -> str:
"""The URL to the program on HackerOne."""
return f"https://hackerone.com/{self.handle}?type=team"
@classmethod
def load_from_dict(cls, program_dict: dict):
"""Initialize class instance from Dictionary object."""
try:
assets = {
HackerOneAsset.load_from_dict(asset)
for asset in program_dict["relationships"]["structured_scopes"]["data"]
}
except KeyError:
# When listing programs the assets are not returned.
assets = set()
return cls(
program_dict["id"],
program_dict["attributes"]["handle"],
program_dict["attributes"]["name"],
program_dict["attributes"]["currency"],
program_dict["attributes"]["profile_picture"],
program_dict["attributes"]["submission_state"],
program_dict["attributes"]["triage_active"],
program_dict["attributes"]["state"],
datetime.fromisoformat(
program_dict["attributes"]["started_accepting_at"].rstrip("Z")
),
program_dict["attributes"]["number_of_reports_for_user"],
program_dict["attributes"]["number_of_valid_reports_for_user"],
program_dict["attributes"]["bounty_earned_for_user"],
program_dict["attributes"]["last_invitation_accepted_at_for_user"],
program_dict["attributes"]["bookmarked"],
program_dict["attributes"]["allows_bounty_splitting"],
program_dict["attributes"]["offers_bounties"],
assets,
)
def __hash__(self):
"""Allow for use in Python Sets."""
return hash(self.id)
def __eq__(self, other):
"""Compare two class instances."""
if other.id == self.id:
return True
return False
class HackerOneSession:
"""Class to interact with the Hacker API of HackerOne."""
def __init__(self, username, token, version="v1"):
self._session = requests.session()
self.version = version
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
"Hello": "HackerOne!",
}
self._session.auth = (username, token)
self._session.headers.update(headers)
def _process_response(self, response):
"""Process HTTP response returned from API."""
if not response.ok:
# TODO: Shall we sleep and retry on 'response.status_code == 429'?
raise IOError(f"HTTP {response.status_code} {response.request.url}")
return response.json()
def _get(self, endpoint, params: dict = None):
"""Retrieve a HTTP GET endpoint."""
url = self._url(endpoint)
response = self._session.get(url, params=params)
return self._process_response(response)
def _url(self, endpoint) -> str:
"""Generate full API url."""
url = f"https://api.hackerone.com/{self.version}/hackers/{endpoint}"
return url
def list_programs(self) -> Set[HackerOneProgram]:
"""Retrieve a list of programs."""
endpoint = "programs"
programs = set()
page_number = 1
while True:
response = self._get(endpoint, params={"page[number]": page_number})
if not response["links"].get("next") or not response.get("data"):
break
else:
page_number += 1
programs.update(
[
HackerOneProgram.load_from_dict(program)
for program in response["data"]
]
)
return programs
def get_program(self, program_handle) -> HackerOneProgram:
"""Retrieve a program by handle."""
endpoint = f"programs/{program_handle}"
response = self._get(endpoint)
return HackerOneProgram.load_from_dict(response)
def get_assets(self, program_handle) -> Set[HackerOneAsset]:
"""Get the assets of given program.
This is a helper function to return only the assets on a program. Useful
when you have retrieved a list of programs as this doesn't include assets.
"""
endpoint = f"programs/{program_handle}"
response = self._get(endpoint)
try:
assets = {
HackerOneAsset.load_from_dict(asset)
for asset in response["relationships"]["structured_scopes"]["data"]
}
except KeyError:
# When listing programs the assets are not returned.
assets = set()
return assets
if __name__ == "__main__":
from getpass import getpass
username = input("Hackerone username: ").strip()
token = getpass(f"{username} token: ").strip()
session = HackerOneSession(username, token)
print(session.get_program("security"))
@martinbydefault
Copy link

Awesome script! Thank you for sharing this!

Just a bug I noticed, the HackerOneAssetType enum is missing this value:
TESTFLIGHT = "TESTFLIGHT"

I noticed the script crashed for some programs having that asset type.

@thiezn
Copy link
Author

thiezn commented Oct 29, 2021

Thanks @martinbydefault ! Must be on a private program I don’t have. Added it to the gist..

@frost19k
Copy link

Line 238, which defines enpoint as "/programs", was causing issues for me. This is because on line 233 url was defined as "https:.../hackers/{endpoint}" which then becomes "https:.../hackers//programs"

Curioiusly, most of the time it's not an issue. Only very rarely did I have a problem.

@thiezn
Copy link
Author

thiezn commented Nov 16, 2021

Thanks @frost19k ! I updated the gist to remove the duplicate slashes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment