Last active
July 27, 2024 11:17
-
-
Save thiezn/eeb78dcdc3902cdb2f33f9050d6d429d to your computer and use it in GitHub Desktop.
HackerOne API Program and scope retrieval
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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")) |
Thanks @martinbydefault ! Must be on a private program I don’t have. Added it to the gist..
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.
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
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.