From bfc3191710c456256ed53d6cd6e5fb8881dc9798 Mon Sep 17 00:00:00 2001 From: thereisnotime <37583483+thereisnotime@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:35:18 +0200 Subject: [PATCH 01/12] Modernize project: uv, ruff, JSON output, justfile, CI - Migrate from pip install scripts to uv with pyproject.toml - Add --json / -j flag for machine-readable output on all commands - Fix os.get_terminal_size() crash when running without a TTY - Suppress mdv terminal width warning by setting COLUMNS before import - Add ruff for linting and formatting, configure in pyproject.toml - Add GitHub Actions workflow for lint checks on push/PR - Add justfile with install, lint, test, run recipes - Rewrite README with proper install/usage docs - Remove install.sh and install.ps1 --- .github/workflows/lint.yml | 28 ++ .gitignore | 11 + .tool-versions | 1 + README.md | 696 +++++++++++++++++-------------------- hackerone.py | 557 ++++++++++++++++++++--------- install.ps1 | 6 - install.sh | 10 - justfile | 51 +++ pyproject.toml | 52 +++ uv.lock | 237 +++++++++++++ 10 files changed, 1076 insertions(+), 573 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 .gitignore create mode 100644 .tool-versions delete mode 100644 install.ps1 delete mode 100644 install.sh create mode 100644 justfile create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..c97b385 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,28 @@ +name: Lint + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Set up Python + run: uv python install 3.10 + + - name: Install dependencies + run: uv sync + + - name: Ruff check + run: uv run ruff check . + + - name: Ruff format check + run: uv run ruff format --check . diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..755e083 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +__pycache__/ +*.py[cod] +*$py.class +*.egg-info/ +dist/ +build/ +.eggs/ +venv/ +.venv/ +.env +.ruff_cache/ diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..07e1593 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +just 1.47.1 diff --git a/README.md b/README.md index fad6e0b..b14cd67 100644 --- a/README.md +++ b/README.md @@ -1,385 +1,311 @@ -# HackerOne CLI Utility - -## Index - -1. [Description](#description) -2. [Usage](#usage) - 2.1. [Windows](#windows) - 2.2. [Unix](#unix) -3. [Installation](#installation) - 3.1. [Requirements](#requirements) - 3.2. [Windows](#windows-1) - 3.3. [Unix](#unix-1) - 3.4. [Tests](#tests) -4. [Modules](#modules) - 4.1. [balance](#balance) - 4.2. [burp](#burp) - 4.3. [csv](#csv) - 4.4. [earnings](#earnings) - 4.5. [help](#help) - 4.6. [payouts](#payouts) - 4.7. [profile](#profile) - 4.8. [program](#program) - 4.9. [programs](#programs) - 4.10. [report](#report) - 4.11. [reports](#reports) - 4.12. [scope](#scope) -5. [License](#license) - -## Description - -This is an (unofficial) utility that works as a client to the HackerOne platform. It allows you to perform multiple operations directly from the command-line. -The tool uses the official [HackerOne API](https://api.hackerone.com/) to access the data it needs. -It contains several modules so you manage and view information related to your profile, reports, programs, payments, etc and it's really easy to use. - -## Usage - -### Windows - -After installing, just run `python3 hackerone.py` to use the utility. - -### Unix - -After installing, you can call `hackerone` directly from the command-line (since a symbolic link will be created during installation) or use python (`python3 hackerone.py`). - -## Installation - -### Requirements - -- Python >= 3.10 -- Git -- HackerOne API Key (you can get it from [here](https://hackerone.com/settings/api_token/edit)) - -### Windows - -1. Open Powershell in the same directory as the project -2. Run the installation file: - - ```ps1 - .\install.ps1 - ``` - -3. Export the required environment variables (don't forget to replace the values between double quotes): - - ```ps1 - echo 'HACKERONE_USERNAME=""' > .env - echo 'HACKERONE_API_KEY=""' >> .env - ``` - - or - - ```ps1 - $env:HACKERONE_USERNAME = "" - $env:HACKERONE_API_KEY = "" - ``` - -### Unix - -1. Open a shell in the same directory as the project -2. Run the installation file: - - ```sh - chmod +x install.sh - ./install.sh - ``` - -3. Export the required environment variables (don't forget to replace the values between double quotes): - - ```sh - echo 'HACKERONE_USERNAME=""' > .env - echo 'HACKERONE_API_KEY=""' >> .env - ``` - - or - - ```sh - export HACKERONE_USERNAME="" - export HACKERONE_API_KEY="" - ``` - -4. Optionally, you can set the environment variables permanently (if you're not using the first option, in the previous process): - - (using ZSH shell) - - ```sh - echo 'export HACKERONE_USERNAME=""' >> .zshrc - echo 'export HACKERONE_API_KEY=""' >> .zshrc - ``` - - (using Bash shell) - - ```sh - echo 'export HACKERONE_USERNAME=""' >> .bashrc - echo 'export HACKERONE_API_KEY=""' >> .bashrc - ``` - -### Tests - -This tool was tested (and performed well) on: - -- Kali Linux 2023.2 + Python 3.11.4 -- Windows 11 (Powershell 7.3.5) + Python 3.11.4 -- Android (with Termux) + Python 3.11.4 - -## Modules - -```txt -balance Check your balance -burp Download the burp configuration file (only from public programs) -csv Download CSV scope file (only from public programs) -earnings Check your earnings -help Help page -payouts Get a list of your payouts -profile Your profile on HackerOne -program Get information from a program -programs [] Get a list of current programs (optional: = maximum number of results) -report Get a specific report -reports Get a list of your reports -scope [] Save a program's scope into a text file (optional: = output file to store results) -``` - -### balance - -Check your money balance. The value provided is in the currency provided on your profile. - -```txt -hackerone balance - -Balance: 1337.0 -``` - -### burp - -Downloads the burp configuration file from public programs. This is not done through the HackerOne API, since there's no such feature, that's why it only works with public programs. -You need to pass the program's handle to use this module. You can find the program handle using the module `programs` or by checking the URL in the browser, while using the HackerOne platform (example: `https://hackerone.com//policy_scopes` in any program) - -```txt -hackerone burp security - -Filename: security-(...).json -``` - -### csv - -Downloads the CSV scope file from public programs. This is not done through the HackerOne API, since there's no such feature, that's why it only works with public programs. -You need to pass the program's handle to use this module. You can find the program handle using the module `programs` or by checking the URL in the browser, while using the HackerOne platform (example: `https://hackerone.com//policy_scopes` in any program) - -```txt -hackerone csv security - -Filename: scopes_for_security_(...).csv -``` - -### earnings - -Check your earnings from the programs you have been. - -```txt -hackerone earnings - -Earnings ----------------------------------------- -Amount: 1337 -Date: 2016-02-02T04:05:06.000Z -Program: HackerOne -Report: RXSS at example.hackerone.com ----------------------------------------- -``` - -### help - -Shows the help page, listing the modules available, their descriptions, and the parameters to be passed. - -### payouts - -Lists all the payouts you had. - -```txt -hackerone payouts - -Payouts ----------------------------------------- -Amount: 1337 -Status: sent -Date: 2016-02-02T04:05:06.000Z -Provider: Paypal ----------------------------------------- -``` - -### profile - -Gets your profile information. It only works if you have any reports, since this module actually checks for the 'reporter' from a report you submitted (there is no user searching / profile feature in the - hacker - API). Unfortunatly it is not possible to get the Signal, Impact or Rank data. - -```txt -hackerone profile - -Profile ----------------------------------------- -ID: 1234567 -Username: example -Reputation: 1337 -Name: Hacker Man -Creation Date: 2020-11-24T16:20:24.066Z -Bio: My beautiful bio -Website: https://example.com/ -Location: Right here ----------------------------------------- -``` - -### program - -Allows you to get information from a program (public or private - if you have authorization) such as the program handle, name, state, creation date, privacy, scope, bounty splitting, bookmarked status and bounty. - -```txt -hackerone program security - -Program ----------------------------------------- -Name: HackerOne -Handle: security -State: open -Availability: Public -Creation date: 2013-11-06T00:00:00.000Z -Bounty: yes -Bounty Splitting: yes -Bookmarked: no - -Scope --------------------- -Asset: hackerone.com -Type: URL -State: In-Scope -Bounty: yes -Instruction: This is our main application that hackers and customers use to interact with each other. It connects with a database that contains information about vulnerability reports, users, and programs. This system’s backend is written in Ruby and exposes data to the client through GraphQL, rendered pages, and JSON endpoints. -Max Severity: critical --------------------- -``` - -### programs - -Gets a list of the most recently updated programs (including the private programs you are in) and some extra information from each. You can pass an extra argument that filters the number of results. The default value is `10`. - -```txt -hackerone programs 2 - -Programs ----------------------------------------- -Program: Example 1 -Handle: example_1 -State: open -Availability: Public -Available since: 2027-07-10T17:10:05.936Z -Bounty Splitting: no -Bookmarked: yes ----------------------------------------- -Program: Example 2 -Handle: example_2 -State: open -Availability: Public -Available since: 2027-05-16T16:00:37.600Z -Bounty Splitting: yes -Bookmarked: no ----------------------------------------- - -Got 2 results! -``` - -### report - -Get most of the information available from a report, including the severity, asset, title, comments, content (only if the report is yours), CVE, CWE, bounties (normal bounty + extra bounty), participants, weakness, etc. -You need to pass the report ID which you can find by using the module `reports` (to get the IDs from your reports) or in a report in the HackerOne website (`https://hackerone.com/reports/`). - -```txt -hackerone report 1234567 - -Report ----------------------------------------- -ID: 1234567 -Title: Stored XSS on example.com -State: resolved -Date: 2027-06-01T00:54:43.308Z -Program: example -Severity: medium -CWE: CWE-79 -Weakness: Cross-site Scripting (XSS) - Stored - -Comments --------------------- -@triager - -Hi! -Thanks for the report, @hacker. We're looking into it. - --------------------- -@triager - -This should now be fixed. Marking as resolved. - --------------------- -@example awarded a bounty (1337.00 + 0.00)! - --------------------- -@hacker - -Hi @triager -Can we disclosure full ? -Thanks - --------------------- -@triager agreed on the report going public! - --------------------- -@triager changed the report visibility to public! - ----------------------------------------- -``` - -### reports - -Get a list of your reports with some information about each one, including the title, ID, state, creation date, CWE, CVSS, weakness, program and severity. - -```txt -hackerone reports - -Reports ----------------------------------------- -ID: 1234567 -Title: Information Exposure through phpinfo() at example.com -State: triaged -Date: 2027-03-13T16:48:17.286Z -CWE: CWE-200 -Program: example -Severity: low -CVSS: 3.7 ----------------------------------------- -ID: 1234568 -Title: RXSS on https://example.com/ via id parameter -State: duplicate -Date: 2027-01-06T12:23:36.605Z -CWE: CWE-79 -Program: example -Severity: high -CVSS: 8.3 ----------------------------------------- -``` - -### scope - -Save a list of in-scope domains, URLs, IP addresses, CIDR addresses, and wildcards in a text file (to use on external tools). This module extracts this info from the csv file available for each program. You need to download it (using the HackerOne website or the `csv` module) and pass the filename as an argument to this module. -Optionally, you can pass the name of the output file as a second argument (default: `inscope.txt`). - -```txt -hackerone scope scopes_for_example.csv out.txt - -In-Scope ----------------------------------------- -*.example.com -127.0.0.0/24 -test.example.com ----------------------------------------- -File 'out.txt' saved! -``` - -## License - -License is available [here](./LICENSE). +# HackerOne CLI Utility + +## Description + +An unofficial CLI client for the [HackerOne](https://hackerone.com/) platform. Manage and view your profile, reports, programs, payments, and more — straight from the terminal. + +Uses the official [HackerOne API](https://api.hackerone.com/) under the hood. + +## Installation + +### Requirements + +- Python >= 3.10 +- [uv](https://docs.astral.sh/uv/getting-started/installation/) (recommended) or pip +- A HackerOne API key — grab one [here](https://hackerone.com/settings/api_token/edit) + +### Install with uv + +```sh +git clone git@github.com:thereisnotime/hackerone-cli.git +cd hackerone-cli +uv sync +``` + +### Install with pip + +```sh +git clone git@github.com:thereisnotime/hackerone-cli.git +cd hackerone-cli +pip install -e . +``` + +### Configuration + +Create a `.env` file in the project directory: + +```sh +HACKERONE_USERNAME="" +HACKERONE_API_KEY="" +``` + +Or export the variables in your shell: + +```sh +export HACKERONE_USERNAME="" +export HACKERONE_API_KEY="" +``` + +## Usage + +With uv: + +```sh +uv run hackerone [args] +``` + +Or if installed via pip: + +```sh +hackerone [args] +``` + +### JSON Output + +Pass `--json` or `-j` to any command to get machine-readable JSON output instead of formatted text. Handy for scripting and piping into other tools. + +```sh +uv run hackerone programs 5 --json +uv run hackerone program security -j +``` + +## Modules + +```txt +balance Check your balance +burp Download the burp configuration file (only from public programs) +csv Download CSV scope file (only from public programs) +earnings Check your earnings +help Help page +payouts Get a list of your payouts +profile Your profile on HackerOne +program Get information from a program +programs [] Get a list of current programs (optional: = maximum number of results) +report Get a specific report +reports Get a list of your reports +scope [] Extract in-scope targets from a CSV scope file into a text file +``` + +### balance + +Check your money balance. The value is in the currency set on your profile. + +```txt +hackerone balance + +Balance: 1337.0 +``` + +### burp + +Downloads the Burp Suite project configuration file from a public program. Not done through the API (no such endpoint), so it only works with public programs. + +Find the program handle via `programs` or from the URL: `https://hackerone.com//policy_scopes`. + +```txt +hackerone burp security + +Filename: security-(...).json +``` + +### csv + +Downloads the CSV scope file from a public program. Same caveat as `burp` — public programs only. + +```txt +hackerone csv security + +Filename: scopes_for_security_(...).csv +``` + +### earnings + +List your bounty earnings. + +```txt +hackerone earnings + +Earnings +---------------------------------------- +Amount: 1337 +Date: 2016-02-02T04:05:06.000Z +Program: HackerOne +Report: RXSS at example.hackerone.com +---------------------------------------- +``` + +### help + +Shows all available modules and their arguments. + +### payouts + +List your payouts. + +```txt +hackerone payouts + +Payouts +---------------------------------------- +Amount: 1337 +Status: sent +Date: 2016-02-02T04:05:06.000Z +Provider: Paypal +---------------------------------------- +``` + +### profile + +Shows your profile info. Only works if you have at least one report (it pulls your profile from the reporter field). + +```txt +hackerone profile + +Profile +---------------------------------------- +ID: 1234567 +Username: example +Reputation: 1337 +Name: Hacker Man +Creation Date: 2020-11-24T16:20:24.066Z +Bio: My beautiful bio +Website: https://example.com/ +Location: Right here +---------------------------------------- +``` + +### program + +Get details about a specific program — scope, policy, bounty info, etc. + +```txt +hackerone program security + +Program +---------------------------------------- +Name: HackerOne +Handle: security +State: open +Availability: Public +Creation date: 2013-11-06T00:00:00.000Z +Bounty: yes +Bounty Splitting: yes +Bookmarked: no + +Scope +-------------------- +Asset: hackerone.com +Type: URL +State: In-Scope +Bounty: yes +Max Severity: critical +-------------------- +``` + +### programs + +List recently updated programs you have access to (including private ones). Defaults to 10 results. + +```txt +hackerone programs 2 + +Programs +---------------------------------------- +Program: Example 1 +Handle: example_1 +State: open +Availability: Public +Available since: 2027-07-10T17:10:05.936Z +Bounty Splitting: no +Bookmarked: yes +---------------------------------------- +Program: Example 2 +Handle: example_2 +State: open +Availability: Public +Available since: 2027-05-16T16:00:37.600Z +Bounty Splitting: yes +Bookmarked: no +---------------------------------------- + +Got 2 results! +``` + +### report + +Get full details on a specific report — severity, comments, bounties, content, CVE/CWE, etc. + +```txt +hackerone report 1234567 + +Report +---------------------------------------- +ID: 1234567 +Title: Stored XSS on example.com +State: resolved +Date: 2027-06-01T00:54:43.308Z +Program: example +Severity: medium +CWE: CWE-79 +Weakness: Cross-site Scripting (XSS) - Stored + +Comments +-------------------- +@triager + +Hi! +Thanks for the report, @hacker. We're looking into it. + +-------------------- +@example awarded a bounty (1337.00 + 0.00)! + +---------------------------------------- +``` + +### reports + +List your submitted reports with key info. + +```txt +hackerone reports + +Reports +---------------------------------------- +ID: 1234567 +Title: Information Exposure through phpinfo() at example.com +State: triaged +Date: 2027-03-13T16:48:17.286Z +CWE: CWE-200 +Program: example +Severity: low +CVSS: 3.7 +---------------------------------------- +``` + +### scope + +Extract in-scope domains, URLs, IPs, CIDRs, and wildcards from a CSV scope file into a text file. Download the CSV first with the `csv` module, then pass it here. + +```txt +hackerone scope scopes_for_example.csv out.txt + +In-Scope +---------------------------------------- +*.example.com +127.0.0.0/24 +test.example.com +---------------------------------------- +File 'out.txt' saved! +``` + +## Development + +```sh +uv sync +uv run ruff check . +uv run ruff format . +``` + +## License + +[MIT](./LICENSE) diff --git a/hackerone.py b/hackerone.py index 22998ff..c2ebf30 100644 --- a/hackerone.py +++ b/hackerone.py @@ -1,14 +1,22 @@ #!/usr/bin/env python3 -import requests -import os -import sys +import csv as csvmod import json -import mdv +import os import re -import csv as csvmod -from requests.auth import HTTPBasicAuth +import sys + +# Set COLUMNS before importing mdv to suppress its "Could not derive terminal width" warning +if "COLUMNS" not in os.environ: + try: + os.environ["COLUMNS"] = str(os.get_terminal_size()[0]) + except OSError: + os.environ["COLUMNS"] = "80" + +import mdv # noqa: E402 +import requests from dotenv import load_dotenv +from requests.auth import HTTPBasicAuth __version__ = "1.0.1" @@ -17,67 +25,110 @@ TOKEN = os.getenv("HACKERONE_API_KEY") auth = HTTPBasicAuth(USERNAME, TOKEN) -def help(): - print("""Modules: - balance Check your balance - burp Download the burp configuration file (only from public programs) - csv Download CSV scope file (only from public programs) - earnings Check your earnings - help Help page - payouts Get a list of your payouts - profile Your profile on HackerOne - program Get information from a program - programs [] Get a list of current programs (optional: = maximum number of results) - report Get a specific report - reports Get a list of your reports - scope [] Extract in-scope domains/URLs/wildcards/IPs/CIDRs from a csv scope file and save it to a text file""") +JSON_OUTPUT = False + + +def _get_terminal_width(default=80): + try: + return os.get_terminal_size()[0] + except OSError: + return default + + +def _error(msg): + if JSON_OUTPUT: + print(json.dumps({"error": msg})) + else: + print(msg) + + +def _error_exit(msg): + _error(msg) + sys.exit() + + +def show_help(): + commands = { + "balance": "Check your balance", + "burp ": "Download the burp configuration file (only from public programs)", + "csv ": "Download CSV scope file (only from public programs)", + "earnings": "Check your earnings", + "help": "Help page", + "payouts": "Get a list of your payouts", + "profile": "Your profile on HackerOne", + "program ": "Get information from a program", + "programs []": "Get a list of current programs (optional: = maximum number of results)", + "report ": "Get a specific report", + "reports": "Get a list of your reports", + "scope []": "Extract in-scope domains/URLs/wildcards/IPs/CIDRs from a csv scope file and save it to a text file", + } + if JSON_OUTPUT: + print(json.dumps({"commands": commands})) + return + print("Modules:") + for cmd, desc in commands.items(): + print(f" {cmd:<32}{desc}") + def burp(): - if (len(sys.argv) != 3): - print("Invalid arguments provided!") + if len(sys.argv) != 3: + _error("Invalid arguments provided!") return - + handler = sys.argv[2] r = requests.get(f"https://hackerone.com/teams/{handler}/assets/download_burp_project_file.json") - if (r.status_code != 200 and r.status_code != 404): - print(f"Request returned {r.status_code}!") - sys.exit() - if (not r.headers["Content-Disposition"].startswith("attachment")): - print(f"Could not find program '{handler}'!") + if r.status_code != 200 and r.status_code != 404: + _error_exit(f"Request returned {r.status_code}!") + if not r.headers["Content-Disposition"].startswith("attachment"): + _error(f"Could not find program '{handler}'!") return - - print("Filename: " + r.headers["Content-Disposition"].split("\"")[1]) - with open(r.headers["Content-Disposition"].split("\"")[1], "wb") as fp: + + filename = r.headers["Content-Disposition"].split('"')[1] + with open(filename, "wb") as fp: fp.write(r.content) + if JSON_OUTPUT: + print(json.dumps({"filename": filename, "status": "downloaded"})) + else: + print("Filename: " + filename) + + def csv(): - if (len(sys.argv) != 3): - print("Invalid arguments provided!") + if len(sys.argv) != 3: + _error("Invalid arguments provided!") return - + handler = sys.argv[2] r = requests.get(f"https://hackerone.com/teams/{handler}/assets/download_csv.csv") - if (r.status_code != 200 and r.status_code != 404): - print(f"Request returned {r.status_code}!") - sys.exit() - if (not r.headers["Content-Disposition"].startswith("attachment")): - print(f"Could not find program '{handler}'!") + if r.status_code != 200 and r.status_code != 404: + _error_exit(f"Request returned {r.status_code}!") + if not r.headers["Content-Disposition"].startswith("attachment"): + _error(f"Could not find program '{handler}'!") return - - print("Filename: " + r.headers["Content-Disposition"].split("\"")[1]) - with open(r.headers["Content-Disposition"].split("\"")[1], "wb") as fp: + + filename = r.headers["Content-Disposition"].split('"')[1] + with open(filename, "wb") as fp: fp.write(r.content) + if JSON_OUTPUT: + print(json.dumps({"filename": filename, "status": "downloaded"})) + else: + print("Filename: " + filename) + + def reports(): r = requests.get("https://api.hackerone.com/v1/hackers/me/reports", auth=auth) - if (r.status_code != 200): - print(f"Request returned {r.status_code}!") - sys.exit() + if r.status_code != 200: + _error_exit(f"Request returned {r.status_code}!") data = json.loads(r.text) - if (len(data["data"]) == 0): + if JSON_OUTPUT: + print(json.dumps(data)) + return + + if len(data["data"]) == 0: print("You have no reports.") return @@ -93,7 +144,7 @@ def reports(): print("Severity: " + rep["relationships"]["severity"]["data"]["attributes"]["rating"]) except: print("Severity: none") - if ("cve_ids" in rep["attributes"] and rep["attributes"]["cve_ids"] not in [None, "", []]): + if "cve_ids" in rep["attributes"] and rep["attributes"]["cve_ids"] not in [None, "", []]: print("CVE: " + ", ".join(rep["attributes"]["cve_ids"])) try: print("CWE: " + str.upper(rep["relationships"]["weakness"]["data"]["attributes"]["external_id"])) @@ -107,28 +158,32 @@ def reports(): pass print("----------------------------------------") + def report(): - if (len(sys.argv) != 3): - print("Invalid arguments provided!") + if len(sys.argv) != 3: + _error("Invalid arguments provided!") return - - if (not sys.argv[2].isdigit()): - print(f"Invalid ID provided '{sys.argv[2]}'!") + + if not sys.argv[2].isdigit(): + _error(f"Invalid ID provided '{sys.argv[2]}'!") return id = sys.argv[2] r = requests.get(f"https://api.hackerone.com/v1/hackers/reports/{id}", auth=auth) - if (r.status_code != 200 and r.status_code != 404): - print(f"Request returned {r.status_code}!") - sys.exit() + if r.status_code != 200 and r.status_code != 404: + _error_exit(f"Request returned {r.status_code}!") data = json.loads(r.text) - if (r.status_code == 404): - print("Report not found!") + if r.status_code == 404: + _error("Report not found!") + return + + if JSON_OUTPUT: + print(json.dumps(data)) return - if (len(data["data"]) == 0): + if len(data["data"]) == 0: print("You have no reports.") return @@ -145,7 +200,7 @@ def report(): print("Severity: " + rep["relationships"]["severity"]["data"]["attributes"]["rating"]) except: print("Severity: none") - if ("cve_ids" in rep["attributes"] and rep["attributes"]["cve_ids"] not in [None, "", []]): + if "cve_ids" in rep["attributes"] and rep["attributes"]["cve_ids"] not in [None, "", []]: print("CVE: " + ", ".join(rep["attributes"]["cve_ids"])) try: print("CWE: " + str.upper(rep["relationships"]["weakness"]["data"]["attributes"]["external_id"])) @@ -153,7 +208,7 @@ def report(): except: print("CWE: none") print("Weakness: none") - + try: print("Asset: " + rep["relationships"]["structured_scope"]["data"]["attributes"]["asset_identifier"]) print("Asset Type: " + rep["relationships"]["structured_scope"]["data"]["attributes"]["asset_type"]) @@ -164,117 +219,219 @@ def report(): except: pass - if ("vulnerability_information" in rep["attributes"]): + if "vulnerability_information" in rep["attributes"]: print("\nContent") print("--------------------") try: - mdv.term_columns = os.get_terminal_size()[0] + mdv.term_columns = _get_terminal_width() print(mdv.main(rep["attributes"]["vulnerability_information"])) except FileNotFoundError: print(rep["attributes"]["vulnerability_information"]) - print("\nComments") for comment in rep["relationships"]["activities"]["data"]: print("--------------------") - if ("username" in comment["relationships"]["actor"]["data"]["attributes"]): + if "username" in comment["relationships"]["actor"]["data"]["attributes"]: entity = comment["relationships"]["actor"]["data"]["attributes"]["username"] - elif ("handle" in comment["relationships"]["actor"]["data"]["attributes"]): + elif "handle" in comment["relationships"]["actor"]["data"]["attributes"]: entity = comment["relationships"]["actor"]["data"]["attributes"]["handle"] else: entity = "someone" try: match comment["type"]: case "activity-report-severity-updated": - print("\x1B[3m@" + entity + "\x1B[23m updated the severity of the report!") + print("\x1b[3m@" + entity + "\x1b[23m updated the severity of the report!") case "activity-bug-pending-program-review": - print("\x1B[3m@" + entity + "\x1B[23m" + ("\n\n" + comment["attributes"]["message"] if "message" in comment["attributes"] and comment["attributes"]["message"] not in [None, ""] else " changed the report status to 'Pending for review'!")) + print( + "\x1b[3m@" + + entity + + "\x1b[23m" + + ( + "\n\n" + comment["attributes"]["message"] + if "message" in comment["attributes"] and comment["attributes"]["message"] not in [None, ""] + else " changed the report status to 'Pending for review'!" + ) + ) case "activity-comment": - print("\x1B[3m@" + entity + "\x1B[23m" + ("\n\n" + comment["attributes"]["message"] if "message" in comment["attributes"] and comment["attributes"]["message"] not in [None, ""] else " posted a comment! (Not visible)")) + print( + "\x1b[3m@" + + entity + + "\x1b[23m" + + ( + "\n\n" + comment["attributes"]["message"] + if "message" in comment["attributes"] and comment["attributes"]["message"] not in [None, ""] + else " posted a comment! (Not visible)" + ) + ) case "activity-bug-triaged": - print("\x1B[3m@" + entity + "\x1B[23m" + ("\n\n" + comment["attributes"]["message"] if "message" in comment["attributes"] and comment["attributes"]["message"] not in [None, ""] else " changed the report status to 'Triaged'!")) + print( + "\x1b[3m@" + + entity + + "\x1b[23m" + + ( + "\n\n" + comment["attributes"]["message"] + if "message" in comment["attributes"] and comment["attributes"]["message"] not in [None, ""] + else " changed the report status to 'Triaged'!" + ) + ) case "activity-bug-resolved": - print("\x1B[3m@" + entity + "\x1B[23m" + ("\n\n" + comment["attributes"]["message"] if "message" in comment["attributes"] and comment["attributes"]["message"] not in [None, ""] else " changed the report status to 'Resolved'!")) + print( + "\x1b[3m@" + + entity + + "\x1b[23m" + + ( + "\n\n" + comment["attributes"]["message"] + if "message" in comment["attributes"] and comment["attributes"]["message"] not in [None, ""] + else " changed the report status to 'Resolved'!" + ) + ) case "activity-bug-duplicate": - print("\x1B[3m@" + entity + "\x1B[23m" + ("\n\n" + comment["attributes"]["message"] if "message" in comment["attributes"] and comment["attributes"]["message"] not in [None, ""] else " changed the report status to 'Duplicate'!")) + print( + "\x1b[3m@" + + entity + + "\x1b[23m" + + ( + "\n\n" + comment["attributes"]["message"] + if "message" in comment["attributes"] and comment["attributes"]["message"] not in [None, ""] + else " changed the report status to 'Duplicate'!" + ) + ) case "activity-bounty-awarded": - print("\x1B[3m@" + rep["relationships"]["program"]["data"]["attributes"]["handle"] + "\x1B[23m" + f" awarded a bounty ({comment['attributes']['bounty_amount']} + {comment['attributes']['bonus_amount']})!" + ("\n\n" + comment["attributes"]["message"] if "message" in comment["attributes"] and comment["attributes"]["message"] not in [None, ""] else "")) + print( + "\x1b[3m@" + + rep["relationships"]["program"]["data"]["attributes"]["handle"] + + "\x1b[23m" + + f" awarded a bounty ({comment['attributes']['bounty_amount']} + {comment['attributes']['bonus_amount']})!" + + ( + "\n\n" + comment["attributes"]["message"] + if "message" in comment["attributes"] and comment["attributes"]["message"] not in [None, ""] + else "" + ) + ) case "activity-bug-retesting": - print("\x1B[3m@" + entity + "\x1B[23m changed the status of the report to 'Retesting'!") + print("\x1b[3m@" + entity + "\x1b[23m changed the status of the report to 'Retesting'!") case "activity-hacker-requested-mediation": - print("\x1B[3m@" + entity + "\x1B[23m has requested mediation from HackerOne Support!") + print("\x1b[3m@" + entity + "\x1b[23m has requested mediation from HackerOne Support!") case "activity-user-completed-retest": - print("\x1B[3m@" + entity + "\x1B[23m" + ("\n\n" + comment["attributes"]["message"] if "message" in comment["attributes"] and comment["attributes"]["message"] not in [None, ""] else " completed retesting!")) + print( + "\x1b[3m@" + + entity + + "\x1b[23m" + + ( + "\n\n" + comment["attributes"]["message"] + if "message" in comment["attributes"] and comment["attributes"]["message"] not in [None, ""] + else " completed retesting!" + ) + ) case "activity-report-retest-approved": - print("\x1B[3m@" + entity + "\x1B[23m approved the retesting!") + print("\x1b[3m@" + entity + "\x1b[23m approved the retesting!") case "activity-report-collaborator-invited": - print("\x1B[3m@" + entity + "\x1B[23m invited a collaborator!") + print("\x1b[3m@" + entity + "\x1b[23m invited a collaborator!") case "activity-report-collaborator-joined": - print("\x1B[3m@" + entity + "\x1B[23m joined as a collaborator!") + print("\x1b[3m@" + entity + "\x1b[23m joined as a collaborator!") case "activity-agreed-on-going-public": - print("\x1B[3m@" + entity + "\x1B[23m" + ("\n\n" + comment["attributes"]["message"] if "message" in comment["attributes"] and comment["attributes"]["message"] not in [None, ""] else " agreed on the report going public!")) + print( + "\x1b[3m@" + + entity + + "\x1b[23m" + + ( + "\n\n" + comment["attributes"]["message"] + if "message" in comment["attributes"] and comment["attributes"]["message"] not in [None, ""] + else " agreed on the report going public!" + ) + ) case "activity-report-became-public": - print("\x1B[3m@" + entity + "\x1B[23m changed the report visibility to public!") + print("\x1b[3m@" + entity + "\x1b[23m changed the report visibility to public!") case "activity-cancelled-disclosure-request": - print("\x1B[3m@" + entity + "\x1B[23m" + ("\n\n" + comment["attributes"]["message"] if "message" in comment["attributes"] and comment["attributes"]["message"] not in [None, ""] else " requested to cancel disclosure!")) + print( + "\x1b[3m@" + + entity + + "\x1b[23m" + + ( + "\n\n" + comment["attributes"]["message"] + if "message" in comment["attributes"] and comment["attributes"]["message"] not in [None, ""] + else " requested to cancel disclosure!" + ) + ) case "activity-report-title-updated": - print("\x1B[3m@" + entity + "\x1B[23m changed the report title!") + print("\x1b[3m@" + entity + "\x1b[23m changed the report title!") case "activity-bug-needs-more-info": - print("\x1B[3m@" + entity + "\x1B[23m changed the report status to 'Needs more info'!") + print("\x1b[3m@" + entity + "\x1b[23m changed the report status to 'Needs more info'!") case "activity-bug-new": - print("\x1B[3m@" + entity + "\x1B[23m changed the report status to 'New'!") + print("\x1b[3m@" + entity + "\x1b[23m changed the report status to 'New'!") case "activity-cve-id-added": - print("\x1B[3m@" + entity + "\x1B[23m added a CVE id!") + print("\x1b[3m@" + entity + "\x1b[23m added a CVE id!") case "activity-external-user-joined": - print("\x1B[3m@" + entity + "\x1B[23m joined this report as a participant!") + print("\x1b[3m@" + entity + "\x1b[23m joined this report as a participant!") case "activity-manually-disclosed": - print("\x1B[3m@" + entity + "\x1B[23m disclosed this report!") + print("\x1b[3m@" + entity + "\x1b[23m disclosed this report!") case "activity-report-vulnerability-types-updated": - print("\x1B[3m@" + entity + "\x1B[23m updated the vulnerability type/weakness!") + print("\x1b[3m@" + entity + "\x1b[23m updated the vulnerability type/weakness!") case _: raise Exception() except: print(comment) - print("\x1B[3m@" + entity + "\x1B[23m participated on the report! (Could not get more details)") + print("\x1b[3m@" + entity + "\x1b[23m participated on the report! (Could not get more details)") print() print("----------------------------------------") + def balance(): r = requests.get("https://api.hackerone.com/v1/hackers/payments/balance", auth=auth) - if (r.status_code != 200): - print(f"Request returned {r.status_code}!") - sys.exit() + if r.status_code != 200: + _error_exit(f"Request returned {r.status_code}!") data = json.loads(r.text) + + if JSON_OUTPUT: + print(json.dumps(data)) + return + print("Balance: " + str(data["data"]["balance"])) + def earnings(): r = requests.get("https://api.hackerone.com/v1/hackers/payments/earnings", auth=auth) - if (r.status_code != 200): - print(f"Request returned {r.status_code}!") - sys.exit() + if r.status_code != 200: + _error_exit(f"Request returned {r.status_code}!") data = json.loads(r.text) - if (len(data["data"]) == 0): + if JSON_OUTPUT: + print(json.dumps(data)) + return + + if len(data["data"]) == 0: print("You have no earnings.") return print("Earnings") print("----------------------------------------") for earn in data["data"]: - print("Amount: " + earn["relationships"]["bounty"]["data"]["attributes"]["amount"] + " " + earn["relationships"]["bounty"]["data"]["attributes"]["awarded_currency"]) + print( + "Amount: " + + earn["relationships"]["bounty"]["data"]["attributes"]["amount"] + + " " + + earn["relationships"]["bounty"]["data"]["attributes"]["awarded_currency"] + ) print("Date: " + earn["attributes"]["created_at"]) print("Program: " + earn["relationships"]["program"]["data"]["attributes"]["name"]) - print("Report: " + earn["relationships"]["bounty"]["data"]["relationships"]["report"]["data"]["attributes"]["title"]) + print( + "Report: " + + earn["relationships"]["bounty"]["data"]["relationships"]["report"]["data"]["attributes"]["title"] + ) print("----------------------------------------") + def payouts(): r = requests.get("https://api.hackerone.com/v1/hackers/payments/payouts", auth=auth) - if (r.status_code != 200): - print(f"Request returned {r.status_code}!") - sys.exit() + if r.status_code != 200: + _error_exit(f"Request returned {r.status_code}!") data = json.loads(r.text) - if (len(data["data"]) == 0): + if JSON_OUTPUT: + print(json.dumps(data)) + return + + if len(data["data"]) == 0: print("You have no payouts.") return @@ -286,46 +443,50 @@ def payouts(): print("Date: " + payout["paid_out_at"]) print("Provider: " + payout["payout_provider"]) + def programs(): - max = 10 - if (len(sys.argv) == 3): - if (sys.argv[2].isdigit() and int(sys.argv[2]) > 0): - max = sys.argv[2] + limit = 10 + if len(sys.argv) == 3: + if sys.argv[2].isdigit() and int(sys.argv[2]) > 0: + limit = sys.argv[2] else: - print(f"Invalid maximum value '{sys.argv[2]}'!") + _error(f"Invalid maximum value '{sys.argv[2]}'!") return - - max = int(max) + + limit = int(limit) c = 0 - programs = [] + results = [] while True: r = requests.get(f"https://api.hackerone.com/v1/hackers/programs?page[size]=100&page[number]={c}", auth=auth) - if (r.status_code != 200 and r.status_code != 404): - print(f"Request returned {r.status_code}!") - sys.exit() + if r.status_code != 200 and r.status_code != 404: + _error_exit(f"Request returned {r.status_code}!") data = json.loads(r.text) - if (r.status_code == 404): + if r.status_code == 404: break - - if (len(data["data"]) == 0): + + if len(data["data"]) == 0: break for program in data["data"]: - programs.append(program) - + results.append(program) + c += 1 - - programs = programs[::-1] + + results = results[::-1] + results = results[:limit] + + if JSON_OUTPUT: + print(json.dumps({"data": results, "count": len(results)})) + return + count = 0 print("Programs") - for program in programs: - if (count == max): - break + for program in results: print("----------------------------------------") print("Program: " + program["attributes"]["name"]) print("Handle: " + program["attributes"]["handle"]) @@ -335,23 +496,27 @@ def programs(): print("Bounty Splitting: " + ("yes" if program["attributes"]["allows_bounty_splitting"] else "no")) print("Bookmarked: " + ("yes" if program["attributes"]["bookmarked"] else "no")) count += 1 - + print("----------------------------------------\n") print(f"Got {count} results!") + def profile(): r = requests.get("https://api.hackerone.com/v1/hackers/me/reports", auth=auth) - if (r.status_code != 200): - print(f"Request returned {r.status_code}!") - sys.exit() + if r.status_code != 200: + _error_exit(f"Request returned {r.status_code}!") data = json.loads(r.text) - if (len(data["data"]) == 0): - print("Could not check your profile!") + if len(data["data"]) == 0: + _error("Could not check your profile!") return user = data["data"][0]["relationships"]["reporter"]["data"] + if JSON_OUTPUT: + print(json.dumps(user)) + return + print("Profile") print("----------------------------------------") print("ID: " + user["id"]) @@ -359,23 +524,48 @@ def profile(): print("Reputation: " + str(user["attributes"]["reputation"])) print("Name: " + user["attributes"]["name"]) print("Creation Date: " + user["attributes"]["created_at"]) - print("Bio: " + (user["attributes"]["bio"] if "bio" in user["attributes"] and user["attributes"]["bio"] not in [None, ""] else "")) - print("Website: " + (user["attributes"]["website"] if "website" in user["attributes"] and user["attributes"]["website"] not in [None, ""] else "")) - print("Location: " + (user["attributes"]["location"] if "location" in user["attributes"] and user["attributes"]["location"] not in [None, ""] else "")) + print( + "Bio: " + + ( + user["attributes"]["bio"] + if "bio" in user["attributes"] and user["attributes"]["bio"] not in [None, ""] + else "" + ) + ) + print( + "Website: " + + ( + user["attributes"]["website"] + if "website" in user["attributes"] and user["attributes"]["website"] not in [None, ""] + else "" + ) + ) + print( + "Location: " + + ( + user["attributes"]["location"] + if "location" in user["attributes"] and user["attributes"]["location"] not in [None, ""] + else "" + ) + ) print("----------------------------------------") + def program(): - if (len(sys.argv) < 3): - print("No handle provided!") + if len(sys.argv) < 3: + _error("No handle provided!") return handle = sys.argv[2] r = requests.get(f"https://api.hackerone.com/v1/hackers/programs/{handle}", auth=auth) - if (r.status_code != 200): - print(f"Request returned {r.status_code}!") - sys.exit() + if r.status_code != 200: + _error_exit(f"Request returned {r.status_code}!") data = json.loads(r.text) + if JSON_OUTPUT: + print(json.dumps(data)) + return + print("Program") print("----------------------------------------") print("Name: " + data["attributes"]["name"]) @@ -390,80 +580,102 @@ def program(): print("\nPolicy") print("--------------------") try: - mdv.term_columns = os.get_terminal_size()[0] + mdv.term_columns = _get_terminal_width() print(mdv.main(data["attributes"]["policy"])) except FileNotFoundError: print(data["attributes"]["policy"]) - + print("\nScope") for scope in data["relationships"]["structured_scopes"]["data"]: print("--------------------") print("Asset: " + scope["attributes"]["asset_identifier"]) print("Type: " + scope["attributes"]["asset_type"]) print("State: " + ("In-Scope" if scope["attributes"]["eligible_for_submission"] else "Out-of-Scope")) - if ("eligible_for_bounty" in scope["attributes"] and scope["attributes"]["eligible_for_bounty"]): + if "eligible_for_bounty" in scope["attributes"] and scope["attributes"]["eligible_for_bounty"]: print("Bounty: " + ("yes" if scope["attributes"]["eligible_for_bounty"] else "no")) - if (scope["attributes"]["instruction"]): + if scope["attributes"]["instruction"]: print("Instruction: " + scope["attributes"]["instruction"]) - print("Max Severity: " + scope["attributes"]["max_severity"] if scope["attributes"]["max_severity"] is not None else "None") + print( + "Max Severity: " + scope["attributes"]["max_severity"] + if scope["attributes"]["max_severity"] is not None + else "None" + ) print("----------------------------------------") + def scope(): - if (len(sys.argv) not in [3,4]): - print(sys.argv) - print("Invalid arguments provided!") + if len(sys.argv) not in [3, 4]: + _error("Invalid arguments provided!") return - - handle = sys.argv[2] + outfile = sys.argv[3] if len(sys.argv) == 4 else "inscope.txt" inscope = [] try: - with open(sys.argv[2], "r") as fp: + with open(sys.argv[2]) as fp: reader = csvmod.reader(fp) try: next(reader) except: raise Exception() - print("In-Scope") - print("----------------------------------------") + if not JSON_OUTPUT: + print("In-Scope") + print("----------------------------------------") for row in reader: - if (not row[4] or row[1] not in ["URL", "DOMAIN", "OTHER", "WILDCARD", "CIDR"]): + if not row[4] or row[1] not in ["URL", "DOMAIN", "OTHER", "WILDCARD", "CIDR"]: continue - if (re.match(r"^(\*\.)?([a-zA-Z0-9\*]([a-zA-Z0-9\-\*]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,256}$", row[0]) or re.match(r"^(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}(\/[0-3]?[0-9])?$", row[0])): - inscope.append(row[0] + "\n") - print(row[0]) - print("----------------------------------------") + if re.match( + r"^(\*\.)?([a-zA-Z0-9\*]([a-zA-Z0-9\-\*]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,256}$", row[0] + ) or re.match( + r"^(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}(\/[0-3]?[0-9])?$", + row[0], + ): + inscope.append(row[0]) + if not JSON_OUTPUT: + print(row[0]) + if not JSON_OUTPUT: + print("----------------------------------------") except: - print(f"Failed to read file '{sys.argv[2]}'!") + _error(f"Failed to read file '{sys.argv[2]}'!") return - + + if JSON_OUTPUT: + print(json.dumps({"inscope": sorted(inscope), "outfile": outfile})) + return + try: with open(f"{outfile}", "a") as fp: - fp.writelines(sorted(inscope)) + fp.writelines(item + "\n" for item in sorted(inscope)) print(f"File '{outfile}' saved!") except: print(f"Failed to write to file '{outfile}'!") return + def main(): - print() + global JSON_OUTPUT + if "--json" in sys.argv or "-j" in sys.argv: + JSON_OUTPUT = True + sys.argv = [a for a in sys.argv if a not in ("--json", "-j")] + + if not JSON_OUTPUT: + print() + if USERNAME is None: - print("Environment variable HACKERONE_USERNAME is not set!") - sys.exit() + _error_exit("Environment variable HACKERONE_USERNAME is not set!") if TOKEN is None: - print("Environment variable HACKERONE_API_KEY is not set!") - sys.exit() - - if (len(sys.argv) < 2): - print("No argument provided!\n") - print(f"Usage: {__file__} help") + _error_exit("Environment variable HACKERONE_API_KEY is not set!") + + if len(sys.argv) < 2: + _error("No argument provided!") + if not JSON_OUTPUT: + print(f"Usage: {__file__} help") sys.exit() - + match sys.argv[1]: case "csv": csv() case "help": - help() + show_help() case "reports": reports() case "report": @@ -485,11 +697,12 @@ def main(): case "scope": scope() case _: - print(f"Invalid module '{sys.argv[1]}'") + _error(f"Invalid module '{sys.argv[1]}'") sys.exit() - + + if __name__ == "__main__": try: main() except KeyboardInterrupt: - print("Exiting...") \ No newline at end of file + print("Exiting...") diff --git a/install.ps1 b/install.ps1 deleted file mode 100644 index 7e6dcc1..0000000 --- a/install.ps1 +++ /dev/null @@ -1,6 +0,0 @@ -pip3 install markdown pygments pyyaml docopt tabulate mdv requests python-dotenv -pip3 install --upgrade --force-reinstall git+http://github.com/axiros/terminal_markdown_viewer -Write-Host -Write-Host "DONE!" -Write-Host -Write-Host "Usage: python3 hackerone.py help" \ No newline at end of file diff --git a/install.sh b/install.sh deleted file mode 100644 index 24f78da..0000000 --- a/install.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -pip3 install markdown pygments pyyaml docopt tabulate mdv requests python-dotenv -pip3 install --upgrade --force-reinstall git+http://github.com/axiros/terminal_markdown_viewer -chmod +x "$(pwd)/hackerone.py" -sudo ln -s "$(pwd)/hackerone.py" /usr/local/bin/hackerone -echo -echo "DONE!" -echo -echo "Usage: hackerone help" \ No newline at end of file diff --git a/justfile b/justfile new file mode 100644 index 0000000..9f856d3 --- /dev/null +++ b/justfile @@ -0,0 +1,51 @@ +# Show available recipes +default: + @just --list + @echo "" + @echo "Run 'just install' to get started." + +# Install dependencies and set up the project +install: + uv sync + +# Run linting checks +lint: + uv run ruff check . + +# Run formatting checks +format-check: + uv run ruff format --check . + +# Auto-fix lint issues +fix: + uv run ruff check --fix . + uv run ruff format . + +# Run all checks (lint + format) +check: lint format-check + +# Run the CLI (pass args after --) +run *ARGS: + uv run hackerone {{ ARGS }} + +# Run the CLI with JSON output (pass args after --) +run-json *ARGS: + uv run hackerone {{ ARGS }} --json + +# Quick smoke test against the API +test: + #!/usr/bin/env bash + set -euo pipefail + echo "Testing help..." + uv run hackerone help > /dev/null + echo "Testing help --json..." + uv run hackerone help --json | python3 -m json.tool > /dev/null + echo "Testing balance --json..." + uv run hackerone balance --json | python3 -m json.tool > /dev/null + echo "Testing programs 1 --json..." + uv run hackerone programs 1 --json | python3 -m json.tool > /dev/null + echo "All tests passed." + +# Clean up generated files +clean: + rm -rf .venv .ruff_cache *.egg-info dist build __pycache__ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..157102c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,52 @@ +[project] +name = "hackerone-cli" +version = "1.0.1" +description = "An unofficial CLI tool for interacting with the HackerOne API" +readme = "README.md" +license = { text = "MIT" } +requires-python = ">=3.10" +dependencies = [ + "requests", + "python-dotenv", + "mdv", +] + +[project.scripts] +hackerone = "hackerone:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.ruff] +target-version = "py310" +line-length = 120 + +[tool.ruff.lint] +select = [ + "E", + "W", + "F", + "I", + "UP", + "B", + "SIM", +] +ignore = [ + "E501", + "SIM105", + "B028", + "E722", + "B904", +] + +[tool.ruff.format] +quote-style = "double" + +[dependency-groups] +dev = [ + "ruff>=0.15.7", +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..cbb7b09 --- /dev/null +++ b/uv.lock @@ -0,0 +1,237 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/8c/2c56124c6dc53a774d435f985b5973bc592f42d437be58c0c92d65ae7296/charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95", size = 298751, upload-time = "2026-03-15T18:50:00.003Z" }, + { url = "https://files.pythonhosted.org/packages/86/2a/2a7db6b314b966a3bcad8c731c0719c60b931b931de7ae9f34b2839289ee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd", size = 200027, upload-time = "2026-03-15T18:50:01.702Z" }, + { url = "https://files.pythonhosted.org/packages/68/f2/0fe775c74ae25e2a3b07b01538fc162737b3e3f795bada3bc26f4d4d495c/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4", size = 220741, upload-time = "2026-03-15T18:50:03.194Z" }, + { url = "https://files.pythonhosted.org/packages/10/98/8085596e41f00b27dd6aa1e68413d1ddda7e605f34dd546833c61fddd709/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db", size = 215802, upload-time = "2026-03-15T18:50:05.859Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ce/865e4e09b041bad659d682bbd98b47fb490b8e124f9398c9448065f64fee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89", size = 207908, upload-time = "2026-03-15T18:50:07.676Z" }, + { url = "https://files.pythonhosted.org/packages/a8/54/8c757f1f7349262898c2f169e0d562b39dcb977503f18fdf0814e923db78/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565", size = 194357, upload-time = "2026-03-15T18:50:09.327Z" }, + { url = "https://files.pythonhosted.org/packages/6f/29/e88f2fac9218907fc7a70722b393d1bbe8334c61fe9c46640dba349b6e66/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9", size = 205610, upload-time = "2026-03-15T18:50:10.732Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c5/21d7bb0cb415287178450171d130bed9d664211fdd59731ed2c34267b07d/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7", size = 203512, upload-time = "2026-03-15T18:50:12.535Z" }, + { url = "https://files.pythonhosted.org/packages/a4/be/ce52f3c7fdb35cc987ad38a53ebcef52eec498f4fb6c66ecfe62cfe57ba2/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550", size = 195398, upload-time = "2026-03-15T18:50:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/81/a0/3ab5dd39d4859a3555e5dadfc8a9fa7f8352f8c183d1a65c90264517da0e/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0", size = 221772, upload-time = "2026-03-15T18:50:15.581Z" }, + { url = "https://files.pythonhosted.org/packages/04/6e/6a4e41a97ba6b2fa87f849c41e4d229449a586be85053c4d90135fe82d26/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8", size = 205759, upload-time = "2026-03-15T18:50:17.047Z" }, + { url = "https://files.pythonhosted.org/packages/db/3b/34a712a5ee64a6957bf355b01dc17b12de457638d436fdb05d01e463cd1c/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0", size = 216938, upload-time = "2026-03-15T18:50:18.44Z" }, + { url = "https://files.pythonhosted.org/packages/cb/05/5bd1e12da9ab18790af05c61aafd01a60f489778179b621ac2a305243c62/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b", size = 210138, upload-time = "2026-03-15T18:50:19.852Z" }, + { url = "https://files.pythonhosted.org/packages/bd/8e/3cb9e2d998ff6b21c0a1860343cb7b83eba9cdb66b91410e18fc4969d6ab/charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557", size = 144137, upload-time = "2026-03-15T18:50:21.505Z" }, + { url = "https://files.pythonhosted.org/packages/d8/8f/78f5489ffadb0db3eb7aff53d31c24531d33eb545f0c6f6567c25f49a5ff/charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6", size = 154244, upload-time = "2026-03-15T18:50:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/e4/74/e472659dffb0cadb2f411282d2d76c60da1fc94076d7fffed4ae8a93ec01/charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058", size = 143312, upload-time = "2026-03-15T18:50:24.074Z" }, + { url = "https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e", size = 293582, upload-time = "2026-03-15T18:50:25.454Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b7/b1a117e5385cbdb3205f6055403c2a2a220c5ea80b8716c324eaf75c5c95/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9", size = 197240, upload-time = "2026-03-15T18:50:27.196Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/2574f0f09f3c3bc1b2f992e20bce6546cb1f17e111c5be07308dc5427956/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d", size = 217363, upload-time = "2026-03-15T18:50:28.601Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d1/0ae20ad77bc949ddd39b51bf383b6ca932f2916074c95cad34ae465ab71f/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de", size = 212994, upload-time = "2026-03-15T18:50:30.102Z" }, + { url = "https://files.pythonhosted.org/packages/60/ac/3233d262a310c1b12633536a07cde5ddd16985e6e7e238e9f3f9423d8eb9/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73", size = 204697, upload-time = "2026-03-15T18:50:31.654Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/8a18fc411f085b82303cfb7154eed5bd49c77035eb7608d049468b53f87c/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c", size = 191673, upload-time = "2026-03-15T18:50:33.433Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a7/11cfe61d6c5c5c7438d6ba40919d0306ed83c9ab957f3d4da2277ff67836/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc", size = 201120, upload-time = "2026-03-15T18:50:35.105Z" }, + { url = "https://files.pythonhosted.org/packages/b5/10/cf491fa1abd47c02f69687046b896c950b92b6cd7337a27e6548adbec8e4/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f", size = 200911, upload-time = "2026-03-15T18:50:36.819Z" }, + { url = "https://files.pythonhosted.org/packages/28/70/039796160b48b18ed466fde0af84c1b090c4e288fae26cd674ad04a2d703/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef", size = 192516, upload-time = "2026-03-15T18:50:38.228Z" }, + { url = "https://files.pythonhosted.org/packages/ff/34/c56f3223393d6ff3124b9e78f7de738047c2d6bc40a4f16ac0c9d7a1cb3c/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398", size = 218795, upload-time = "2026-03-15T18:50:39.664Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3b/ce2d4f86c5282191a041fdc5a4ce18f1c6bd40a5bd1f74cf8625f08d51c1/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e", size = 201833, upload-time = "2026-03-15T18:50:41.552Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9b/b6a9f76b0fd7c5b5ec58b228ff7e85095370282150f0bd50b3126f5506d6/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed", size = 213920, upload-time = "2026-03-15T18:50:43.33Z" }, + { url = "https://files.pythonhosted.org/packages/ae/98/7bc23513a33d8172365ed30ee3a3b3fe1ece14a395e5fc94129541fc6003/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021", size = 206951, upload-time = "2026-03-15T18:50:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/32/73/c0b86f3d1458468e11aec870e6b3feac931facbe105a894b552b0e518e79/charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e", size = 143703, upload-time = "2026-03-15T18:50:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e3/76f2facfe8eddee0bbd38d2594e709033338eae44ebf1738bcefe0a06185/charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4", size = 153857, upload-time = "2026-03-15T18:50:47.563Z" }, + { url = "https://files.pythonhosted.org/packages/e2/dc/9abe19c9b27e6cd3636036b9d1b387b78c40dedbf0b47f9366737684b4b0/charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316", size = 142751, upload-time = "2026-03-15T18:50:49.234Z" }, + { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, + { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, + { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, + { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, + { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, + { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, + { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, + { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, + { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, + { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +] + +[[package]] +name = "hackerone-cli" +version = "1.0.1" +source = { editable = "." } +dependencies = [ + { name = "mdv" }, + { name = "python-dotenv" }, + { name = "requests" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "mdv" }, + { name = "python-dotenv" }, + { name = "requests" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "ruff", specifier = ">=0.15.7" }] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "mdv" +version = "1.7.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/32/f5e1b8c70dc40b02604fbd0be3ff0bd5e01ee99c9fddf8f423b10d07cd31/mdv-1.7.5.tar.gz", hash = "sha256:eb84ed52a2b68d2e083e007cb485d14fac1deb755fd8f35011eff8f2889df6e9", size = 54174, upload-time = "2023-10-02T21:19:34.482Z" } + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "requests" +version = "2.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" }, + { url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433, upload-time = "2026-03-19T16:27:00.205Z" }, + { url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" }, + { url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536, upload-time = "2026-03-19T16:27:06.101Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" }, + { url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752, upload-time = "2026-03-19T16:26:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" }, + { url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538, upload-time = "2026-03-19T16:26:15.992Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839, upload-time = "2026-03-19T16:26:19.897Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] From 64a760c0be845a5dfd25b75ea3989e0ba7ee2f4a Mon Sep 17 00:00:00 2001 From: thereisnotime <37583483+thereisnotime@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:37:52 +0200 Subject: [PATCH 02/12] Clean up README with proper structure and navigation Table of contents, command reference table, grouped modules by category, quick start section, just recipes in dev section. --- README.md | 396 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 230 insertions(+), 166 deletions(-) diff --git a/README.md b/README.md index b14cd67..92ca29a 100644 --- a/README.md +++ b/README.md @@ -1,185 +1,172 @@ -# HackerOne CLI Utility +# HackerOne CLI -## Description +An unofficial CLI client for the [HackerOne](https://hackerone.com/) platform. Manage your profile, reports, programs, payments, and more — straight from the terminal. -An unofficial CLI client for the [HackerOne](https://hackerone.com/) platform. Manage and view your profile, reports, programs, payments, and more — straight from the terminal. +Built on the official [HackerOne API v1](https://api.hackerone.com/). -Uses the official [HackerOne API](https://api.hackerone.com/) under the hood. +--- -## Installation +## Table of Contents -### Requirements +- [Quick Start](#quick-start) +- [Installation](#installation) +- [Configuration](#configuration) +- [Usage](#usage) +- [Modules](#modules) + - [Programs & Scope](#programs--scope) — `programs`, `program`, `scope`, `csv`, `burp` + - [Reports](#reports) — `reports`, `report` + - [Payments](#payments) — `balance`, `earnings`, `payouts` + - [Account](#account) — `profile`, `help` +- [Development](#development) +- [License](#license) -- Python >= 3.10 -- [uv](https://docs.astral.sh/uv/getting-started/installation/) (recommended) or pip -- A HackerOne API key — grab one [here](https://hackerone.com/settings/api_token/edit) +--- -### Install with uv +## Quick Start ```sh git clone git@github.com:thereisnotime/hackerone-cli.git cd hackerone-cli uv sync +echo 'HACKERONE_USERNAME="your-username"' > .env +echo 'HACKERONE_API_KEY="your-api-key"' >> .env +uv run hackerone programs 5 ``` -### Install with pip +--- -```sh -git clone git@github.com:thereisnotime/hackerone-cli.git -cd hackerone-cli -pip install -e . -``` +## Installation -### Configuration +**Requirements:** Python >= 3.10, a [HackerOne API key](https://hackerone.com/settings/api_token/edit) -Create a `.env` file in the project directory: +### With uv (recommended) ```sh -HACKERONE_USERNAME="" -HACKERONE_API_KEY="" +git clone git@github.com:thereisnotime/hackerone-cli.git +cd hackerone-cli +uv sync ``` -Or export the variables in your shell: +### With pip ```sh -export HACKERONE_USERNAME="" -export HACKERONE_API_KEY="" +git clone git@github.com:thereisnotime/hackerone-cli.git +cd hackerone-cli +pip install -e . ``` -## Usage +### With just -With uv: +If you have [just](https://github.com/casey/just) installed: ```sh -uv run hackerone [args] +just install ``` -Or if installed via pip: +Run `just` to see all available recipes. -```sh -hackerone [args] -``` +--- -### JSON Output +## Configuration -Pass `--json` or `-j` to any command to get machine-readable JSON output instead of formatted text. Handy for scripting and piping into other tools. +Create a `.env` file in the project root: ```sh -uv run hackerone programs 5 --json -uv run hackerone program security -j +HACKERONE_USERNAME="your-username" +HACKERONE_API_KEY="your-api-key" ``` -## Modules +Or export them directly: -```txt -balance Check your balance -burp Download the burp configuration file (only from public programs) -csv Download CSV scope file (only from public programs) -earnings Check your earnings -help Help page -payouts Get a list of your payouts -profile Your profile on HackerOne -program Get information from a program -programs [] Get a list of current programs (optional: = maximum number of results) -report Get a specific report -reports Get a list of your reports -scope [] Extract in-scope targets from a CSV scope file into a text file +```sh +export HACKERONE_USERNAME="your-username" +export HACKERONE_API_KEY="your-api-key" ``` -### balance - -Check your money balance. The value is in the currency set on your profile. - -```txt -hackerone balance - -Balance: 1337.0 -``` +You can get your API key from [HackerOne Settings](https://hackerone.com/settings/api_token/edit). -### burp +--- -Downloads the Burp Suite project configuration file from a public program. Not done through the API (no such endpoint), so it only works with public programs. +## Usage -Find the program handle via `programs` or from the URL: `https://hackerone.com//policy_scopes`. +```sh +# With uv +uv run hackerone [args] -```txt -hackerone burp security +# With pip install +hackerone [args] -Filename: security-(...).json +# With just +just run [args] ``` -### csv - -Downloads the CSV scope file from a public program. Same caveat as `burp` — public programs only. +### JSON Output -```txt -hackerone csv security +Any command supports `--json` (or `-j`) for machine-readable output. Great for scripting, piping into `jq`, or integrating with other tools. -Filename: scopes_for_security_(...).csv +```sh +uv run hackerone programs 5 --json +uv run hackerone program security -j | jq '.attributes.handle' ``` -### earnings - -List your bounty earnings. +--- -```txt -hackerone earnings - -Earnings ----------------------------------------- -Amount: 1337 -Date: 2016-02-02T04:05:06.000Z -Program: HackerOne -Report: RXSS at example.hackerone.com ----------------------------------------- -``` +## Modules -### help +| Command | Description | +|---|---| +| `programs [max]` | List programs you have access to (default: 10) | +| `program ` | Details on a specific program (scope, policy, bounty info) | +| `scope [outfile]` | Extract in-scope targets from a CSV file | +| `csv ` | Download CSV scope file (public programs only) | +| `burp ` | Download Burp Suite config (public programs only) | +| `reports` | List your submitted reports | +| `report ` | Full details on a specific report | +| `balance` | Check your current balance | +| `earnings` | List your bounty earnings | +| `payouts` | List your payouts | +| `profile` | Your profile info | +| `help` | Show available commands | -Shows all available modules and their arguments. +### Programs & Scope -### payouts +#### `programs [max]` -List your payouts. +List recently updated programs you have access to, including private ones. Defaults to 10 results. -```txt -hackerone payouts +``` +$ hackerone programs 2 -Payouts +Programs ---------------------------------------- -Amount: 1337 -Status: sent -Date: 2016-02-02T04:05:06.000Z -Provider: Paypal +Program: Example 1 +Handle: example_1 +State: open +Availability: Public +Available since: 2027-07-10T17:10:05.936Z +Bounty Splitting: no +Bookmarked: yes ---------------------------------------- +Program: Example 2 +Handle: example_2 +State: open +Availability: Public +Available since: 2027-05-16T16:00:37.600Z +Bounty Splitting: yes +Bookmarked: no +---------------------------------------- + +Got 2 results! ``` -### profile +#### `program ` -Shows your profile info. Only works if you have at least one report (it pulls your profile from the reporter field). +Get details about a specific program — scope, policy, bounty info. Works with both public and private programs (if you have access). -```txt -hackerone profile +Find the handle via `programs` or from the URL: `https://hackerone.com/`. -Profile ----------------------------------------- -ID: 1234567 -Username: example -Reputation: 1337 -Name: Hacker Man -Creation Date: 2020-11-24T16:20:24.066Z -Bio: My beautiful bio -Website: https://example.com/ -Location: Right here ----------------------------------------- ``` - -### program - -Get details about a specific program — scope, policy, bounty info, etc. - -```txt -hackerone program security +$ hackerone program security Program ---------------------------------------- @@ -190,7 +177,6 @@ Availability: Public Creation date: 2013-11-06T00:00:00.000Z Bounty: yes Bounty Splitting: yes -Bookmarked: no Scope -------------------- @@ -202,41 +188,72 @@ Max Severity: critical -------------------- ``` -### programs +#### `csv ` -List recently updated programs you have access to (including private ones). Defaults to 10 results. +Download the CSV scope file from a public program. This uses a web endpoint, not the API. -```txt -hackerone programs 2 +``` +$ hackerone csv security -Programs ----------------------------------------- -Program: Example 1 -Handle: example_1 -State: open -Availability: Public -Available since: 2027-07-10T17:10:05.936Z -Bounty Splitting: no -Bookmarked: yes +Filename: scopes_for_security_(...).csv +``` + +#### `scope [outfile]` + +Parse a downloaded CSV scope file and extract in-scope domains, URLs, IPs, CIDRs, and wildcards into a text file. Useful for feeding into other recon tools. + +Output file defaults to `inscope.txt`. + +``` +$ hackerone scope scopes_for_example.csv targets.txt + +In-Scope ---------------------------------------- -Program: Example 2 -Handle: example_2 -State: open -Availability: Public -Available since: 2027-05-16T16:00:37.600Z -Bounty Splitting: yes -Bookmarked: no +*.example.com +127.0.0.0/24 +test.example.com ---------------------------------------- +File 'targets.txt' saved! +``` -Got 2 results! +#### `burp ` + +Download the Burp Suite project configuration file from a public program. + +``` +$ hackerone burp security + +Filename: security-(...).json ``` -### report +### Reports + +#### `reports` -Get full details on a specific report — severity, comments, bounties, content, CVE/CWE, etc. +List your submitted reports with key metadata. -```txt -hackerone report 1234567 +``` +$ hackerone reports + +Reports +---------------------------------------- +ID: 1234567 +Title: Information Exposure through phpinfo() at example.com +State: triaged +Date: 2027-03-13T16:48:17.286Z +CWE: CWE-200 +Program: example +Severity: low +CVSS: 3.7 +---------------------------------------- +``` + +#### `report ` + +Get the full details on a specific report — severity, comments, bounties, content, CVE/CWE, and more. + +``` +$ hackerone report 1234567 Report ---------------------------------------- @@ -253,8 +270,7 @@ Comments -------------------- @triager -Hi! -Thanks for the report, @hacker. We're looking into it. +Thanks for the report. We're looking into it. -------------------- @example awarded a bounty (1337.00 + 0.00)! @@ -262,50 +278,98 @@ Thanks for the report, @hacker. We're looking into it. ---------------------------------------- ``` -### reports +### Payments -List your submitted reports with key info. +#### `balance` -```txt -hackerone reports +Check your current balance (in the currency set on your profile). -Reports +``` +$ hackerone balance + +Balance: 1337.0 +``` + +#### `earnings` + +List your bounty earnings by program. + +``` +$ hackerone earnings + +Earnings ---------------------------------------- -ID: 1234567 -Title: Information Exposure through phpinfo() at example.com -State: triaged -Date: 2027-03-13T16:48:17.286Z -CWE: CWE-200 -Program: example -Severity: low -CVSS: 3.7 +Amount: 1337 USD +Date: 2016-02-02T04:05:06.000Z +Program: HackerOne +Report: RXSS at example.hackerone.com ---------------------------------------- ``` -### scope +#### `payouts` -Extract in-scope domains, URLs, IPs, CIDRs, and wildcards from a CSV scope file into a text file. Download the CSV first with the `csv` module, then pass it here. +List your processed payouts. -```txt -hackerone scope scopes_for_example.csv out.txt +``` +$ hackerone payouts -In-Scope +Payouts ---------------------------------------- -*.example.com -127.0.0.0/24 -test.example.com +Amount: 1337 +Status: sent +Date: 2016-02-02T04:05:06.000Z +Provider: Paypal ---------------------------------------- -File 'out.txt' saved! ``` +### Account + +#### `profile` + +Show your profile info. Requires at least one submitted report (the API derives your profile from the reporter field). + +``` +$ hackerone profile + +Profile +---------------------------------------- +ID: 1234567 +Username: example +Reputation: 1337 +Name: Hacker Man +Creation Date: 2020-11-24T16:20:24.066Z +Bio: My beautiful bio +Website: https://example.com/ +Location: Right here +---------------------------------------- +``` + +#### `help` + +Show all available commands and their arguments. + +--- + ## Development +```sh +just install # Install dependencies +just check # Run lint + format checks +just fix # Auto-fix lint and formatting +just test # Smoke test against the live API +just run # Run the CLI +``` + +Or without just: + ```sh uv sync uv run ruff check . uv run ruff format . ``` +--- + ## License [MIT](./LICENSE) From a6ff4fd50bce2c1402a342b2450503a34e7023fa Mon Sep 17 00:00:00 2001 From: thereisnotime <37583483+thereisnotime@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:58:06 +0200 Subject: [PATCH 03/12] Add CLI flags for credentials and custom env file Support --username/-u, --api-key/-k, and --env-file as alternatives to environment variables and .env. CLI flags take priority over env. --- README.md | 42 +++++++++++++++++++++++++++++++++--------- hackerone.py | 43 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 92ca29a..b7dbe07 100644 --- a/README.md +++ b/README.md @@ -69,21 +69,36 @@ Run `just` to see all available recipes. ## Configuration -Create a `.env` file in the project root: +Credentials can be provided in three ways (in order of priority): + +**1. CLI flags** (highest priority): ```sh -HACKERONE_USERNAME="your-username" -HACKERONE_API_KEY="your-api-key" +hackerone --username myuser --api-key mykey programs +hackerone -u myuser -k mykey balance ``` -Or export them directly: +**2. Environment variables**: ```sh export HACKERONE_USERNAME="your-username" export HACKERONE_API_KEY="your-api-key" ``` -You can get your API key from [HackerOne Settings](https://hackerone.com/settings/api_token/edit). +**3. `.env` file** (auto-loaded from the current directory): + +```sh +HACKERONE_USERNAME="your-username" +HACKERONE_API_KEY="your-api-key" +``` + +You can also point to a custom `.env` file: + +```sh +hackerone --env-file /path/to/.env programs +``` + +Get your API key from [HackerOne Settings](https://hackerone.com/settings/api_token/edit). --- @@ -91,22 +106,31 @@ You can get your API key from [HackerOne Settings](https://hackerone.com/setting ```sh # With uv -uv run hackerone [args] +uv run hackerone [args] [options] # With pip install -hackerone [args] +hackerone [args] [options] # With just just run [args] ``` +### Global Options + +| Flag | Short | Description | +|---|---|---| +| `--json` | `-j` | Machine-readable JSON output | +| `--username ` | `-u` | HackerOne username (overrides env) | +| `--api-key ` | `-k` | HackerOne API key (overrides env) | +| `--env-file ` | | Path to a custom `.env` file | + ### JSON Output Any command supports `--json` (or `-j`) for machine-readable output. Great for scripting, piping into `jq`, or integrating with other tools. ```sh -uv run hackerone programs 5 --json -uv run hackerone program security -j | jq '.attributes.handle' +hackerone programs 5 --json +hackerone program security -j | jq '.attributes.handle' ``` --- diff --git a/hackerone.py b/hackerone.py index c2ebf30..fdb8b03 100644 --- a/hackerone.py +++ b/hackerone.py @@ -21,11 +21,9 @@ __version__ = "1.0.1" load_dotenv() -USERNAME = os.getenv("HACKERONE_USERNAME") -TOKEN = os.getenv("HACKERONE_API_KEY") -auth = HTTPBasicAuth(USERNAME, TOKEN) JSON_OUTPUT = False +auth = None def _get_terminal_width(default=80): @@ -68,6 +66,11 @@ def show_help(): print("Modules:") for cmd, desc in commands.items(): print(f" {cmd:<32}{desc}") + print("\nOptions:") + print(" --username, -u HackerOne username (overrides HACKERONE_USERNAME)") + print(" --api-key, -k HackerOne API key (overrides HACKERONE_API_KEY)") + print(" --json, -j Output as JSON") + print(" --env-file Path to .env file (default: .env in current directory)") def burp(): @@ -651,19 +654,43 @@ def scope(): return +def _extract_flag(flag, *aliases): + """Extract a --flag value from sys.argv, removing both the flag and its value.""" + for name in (flag, *aliases): + if name in sys.argv: + idx = sys.argv.index(name) + if idx + 1 < len(sys.argv): + value = sys.argv[idx + 1] + sys.argv = sys.argv[:idx] + sys.argv[idx + 2 :] + return value + else: + sys.argv = sys.argv[:idx] + sys.argv[idx + 1 :] + return None + return None + + def main(): - global JSON_OUTPUT + global JSON_OUTPUT, auth if "--json" in sys.argv or "-j" in sys.argv: JSON_OUTPUT = True sys.argv = [a for a in sys.argv if a not in ("--json", "-j")] + env_file = _extract_flag("--env-file") + if env_file: + load_dotenv(env_file, override=True) + + username = _extract_flag("--username", "-u") or os.getenv("HACKERONE_USERNAME") + api_key = _extract_flag("--api-key", "-k") or os.getenv("HACKERONE_API_KEY") + if not JSON_OUTPUT: print() - if USERNAME is None: - _error_exit("Environment variable HACKERONE_USERNAME is not set!") - if TOKEN is None: - _error_exit("Environment variable HACKERONE_API_KEY is not set!") + if username is None: + _error_exit("No username provided! Use --username or set HACKERONE_USERNAME.") + if api_key is None: + _error_exit("No API key provided! Use --api-key or set HACKERONE_API_KEY.") + + auth = HTTPBasicAuth(username, api_key) if len(sys.argv) < 2: _error("No argument provided!") From fd0f086d1572ef15a0fe78048d471180065acfb0 Mon Sep 17 00:00:00 2001 From: thereisnotime <37583483+thereisnotime@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:59:59 +0200 Subject: [PATCH 04/12] Restructure README for better navigation Collapsible install methods and example outputs, clickable TOC with direct links to each command, command reference table with anchor links, credentials as a clean priority table. --- README.md | 178 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 101 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index b7dbe07..3240de1 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,29 @@ # HackerOne CLI -An unofficial CLI client for the [HackerOne](https://hackerone.com/) platform. Manage your profile, reports, programs, payments, and more — straight from the terminal. - -Built on the official [HackerOne API v1](https://api.hackerone.com/). - ---- +An unofficial CLI client for [HackerOne](https://hackerone.com/). Manage your profile, reports, programs, payments, and more from the terminal. Built on the official [HackerOne API v1](https://api.hackerone.com/). ## Table of Contents - [Quick Start](#quick-start) - [Installation](#installation) - [Configuration](#configuration) + - [Credentials](#credentials) + - [Custom .env File](#custom-env-file) - [Usage](#usage) -- [Modules](#modules) - - [Programs & Scope](#programs--scope) — `programs`, `program`, `scope`, `csv`, `burp` - - [Reports](#reports) — `reports`, `report` - - [Payments](#payments) — `balance`, `earnings`, `payouts` - - [Account](#account) — `profile`, `help` + - [Global Options](#global-options) + - [JSON Output](#json-output) +- [Commands](#commands) + - [Programs & Scope](#programs--scope) + - [`programs`](#programs-max) | [`program`](#program-handle) | [`csv`](#csv-handle) | [`scope`](#scope-csv-outfile) | [`burp`](#burp-handle) + - [Reports](#reports) + - [`reports`](#reports-1) | [`report`](#report-id) + - [Payments](#payments) + - [`balance`](#balance) | [`earnings`](#earnings) | [`payouts`](#payouts) + - [Account](#account) + - [`profile`](#profile) | [`help`](#help) - [Development](#development) - [License](#license) ---- - ## Quick Start ```sh @@ -33,13 +35,12 @@ echo 'HACKERONE_API_KEY="your-api-key"' >> .env uv run hackerone programs 5 ``` ---- - ## Installation **Requirements:** Python >= 3.10, a [HackerOne API key](https://hackerone.com/settings/api_token/edit) -### With uv (recommended) +
+With uv (recommended) ```sh git clone git@github.com:thereisnotime/hackerone-cli.git @@ -47,7 +48,10 @@ cd hackerone-cli uv sync ``` -### With pip +
+ +
+With pip ```sh git clone git@github.com:thereisnotime/hackerone-cli.git @@ -55,9 +59,10 @@ cd hackerone-cli pip install -e . ``` -### With just +
-If you have [just](https://github.com/casey/just) installed: +
+With just ```sh just install @@ -65,34 +70,21 @@ just install Run `just` to see all available recipes. ---- +
## Configuration -Credentials can be provided in three ways (in order of priority): - -**1. CLI flags** (highest priority): - -```sh -hackerone --username myuser --api-key mykey programs -hackerone -u myuser -k mykey balance -``` - -**2. Environment variables**: +### Credentials -```sh -export HACKERONE_USERNAME="your-username" -export HACKERONE_API_KEY="your-api-key" -``` +Credentials are resolved in this order (first match wins): -**3. `.env` file** (auto-loaded from the current directory): - -```sh -HACKERONE_USERNAME="your-username" -HACKERONE_API_KEY="your-api-key" -``` +| Method | Example | +|---|---| +| CLI flags | `hackerone -u myuser -k mykey programs` | +| Environment variables | `export HACKERONE_USERNAME="..."` / `export HACKERONE_API_KEY="..."` | +| `.env` file | Auto-loaded from the current directory | -You can also point to a custom `.env` file: +### Custom .env File ```sh hackerone --env-file /path/to/.env programs @@ -100,19 +92,12 @@ hackerone --env-file /path/to/.env programs Get your API key from [HackerOne Settings](https://hackerone.com/settings/api_token/edit). ---- - ## Usage ```sh -# With uv -uv run hackerone [args] [options] - -# With pip install -hackerone [args] [options] - -# With just -just run [args] +uv run hackerone [args] [options] # with uv +hackerone [args] [options] # with pip +just run [args] # with just ``` ### Global Options @@ -126,31 +111,33 @@ just run [args] ### JSON Output -Any command supports `--json` (or `-j`) for machine-readable output. Great for scripting, piping into `jq`, or integrating with other tools. +Any command supports `--json` / `-j` for machine-readable output. Useful for scripting, piping into `jq`, or integrating with other tools. ```sh hackerone programs 5 --json hackerone program security -j | jq '.attributes.handle' ``` ---- +## Commands -## Modules +### Command Reference | Command | Description | |---|---| -| `programs [max]` | List programs you have access to (default: 10) | -| `program ` | Details on a specific program (scope, policy, bounty info) | -| `scope [outfile]` | Extract in-scope targets from a CSV file | -| `csv ` | Download CSV scope file (public programs only) | -| `burp ` | Download Burp Suite config (public programs only) | -| `reports` | List your submitted reports | -| `report ` | Full details on a specific report | -| `balance` | Check your current balance | -| `earnings` | List your bounty earnings | -| `payouts` | List your payouts | -| `profile` | Your profile info | -| `help` | Show available commands | +| [`programs [max]`](#programs-max) | List programs you have access to (default: 10) | +| [`program `](#program-handle) | Program details — scope, policy, bounty info | +| [`csv `](#csv-handle) | Download CSV scope file (public only) | +| [`scope [outfile]`](#scope-csv-outfile) | Extract in-scope targets from CSV | +| [`burp `](#burp-handle) | Download Burp Suite config (public only) | +| [`reports`](#reports-1) | List your submitted reports | +| [`report `](#report-id) | Full report details | +| [`balance`](#balance) | Current balance | +| [`earnings`](#earnings) | Bounty earnings | +| [`payouts`](#payouts) | Payout history | +| [`profile`](#profile) | Your profile info | +| [`help`](#help) | Show available commands | + +--- ### Programs & Scope @@ -158,6 +145,9 @@ hackerone program security -j | jq '.attributes.handle' List recently updated programs you have access to, including private ones. Defaults to 10 results. +
+Example output + ``` $ hackerone programs 2 @@ -183,12 +173,17 @@ Bookmarked: no Got 2 results! ``` +
+ #### `program ` -Get details about a specific program — scope, policy, bounty info. Works with both public and private programs (if you have access). +Get details about a specific program — scope, policy, bounty info. Works with both public and private programs you have access to. Find the handle via `programs` or from the URL: `https://hackerone.com/`. +
+Example output + ``` $ hackerone program security @@ -212,21 +207,23 @@ Max Severity: critical -------------------- ``` +
+ #### `csv ` -Download the CSV scope file from a public program. This uses a web endpoint, not the API. +Download the CSV scope file from a public program. Uses a web endpoint (not the API), so only works with public programs. ``` $ hackerone csv security - Filename: scopes_for_security_(...).csv ``` #### `scope [outfile]` -Parse a downloaded CSV scope file and extract in-scope domains, URLs, IPs, CIDRs, and wildcards into a text file. Useful for feeding into other recon tools. +Extract in-scope domains, URLs, IPs, CIDRs, and wildcards from a downloaded CSV scope file. Output defaults to `inscope.txt`. Useful for feeding into recon tools. -Output file defaults to `inscope.txt`. +
+Example output ``` $ hackerone scope scopes_for_example.csv targets.txt @@ -240,22 +237,28 @@ test.example.com File 'targets.txt' saved! ``` +
+ #### `burp ` Download the Burp Suite project configuration file from a public program. ``` $ hackerone burp security - Filename: security-(...).json ``` +--- + ### Reports #### `reports` List your submitted reports with key metadata. +
+Example output + ``` $ hackerone reports @@ -272,9 +275,14 @@ CVSS: 3.7 ---------------------------------------- ``` +
+ #### `report ` -Get the full details on a specific report — severity, comments, bounties, content, CVE/CWE, and more. +Full details on a specific report — severity, comments, bounties, content, CVE/CWE, and more. + +
+Example output ``` $ hackerone report 1234567 @@ -302,6 +310,10 @@ Thanks for the report. We're looking into it. ---------------------------------------- ``` +
+ +--- + ### Payments #### `balance` @@ -310,7 +322,6 @@ Check your current balance (in the currency set on your profile). ``` $ hackerone balance - Balance: 1337.0 ``` @@ -318,6 +329,9 @@ Balance: 1337.0 List your bounty earnings by program. +
+Example output + ``` $ hackerone earnings @@ -330,10 +344,15 @@ Report: RXSS at example.hackerone.com ---------------------------------------- ``` +
+ #### `payouts` List your processed payouts. +
+Example output + ``` $ hackerone payouts @@ -346,12 +365,19 @@ Provider: Paypal ---------------------------------------- ``` +
+ +--- + ### Account #### `profile` Show your profile info. Requires at least one submitted report (the API derives your profile from the reporter field). +
+Example output + ``` $ hackerone profile @@ -368,12 +394,12 @@ Location: Right here ---------------------------------------- ``` +
+ #### `help` Show all available commands and their arguments. ---- - ## Development ```sh @@ -392,8 +418,6 @@ uv run ruff check . uv run ruff format . ``` ---- - ## License [MIT](./LICENSE) From d578c6fd4676a2d853a833b43d5fcb5d14ba7c8d Mon Sep 17 00:00:00 2001 From: thereisnotime <37583483+thereisnotime@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:04:21 +0200 Subject: [PATCH 05/12] Default to global install with uv tool install Most people want to just type 'hackerone' directly, not prefix everything with 'uv run'. Moved uv sync to alternative methods. --- README.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 3240de1..f67012d 100644 --- a/README.md +++ b/README.md @@ -29,47 +29,49 @@ An unofficial CLI client for [HackerOne](https://hackerone.com/). Manage your pr ```sh git clone git@github.com:thereisnotime/hackerone-cli.git cd hackerone-cli -uv sync +uv tool install -e . echo 'HACKERONE_USERNAME="your-username"' > .env echo 'HACKERONE_API_KEY="your-api-key"' >> .env -uv run hackerone programs 5 +hackerone programs 5 ``` ## Installation **Requirements:** Python >= 3.10, a [HackerOne API key](https://hackerone.com/settings/api_token/edit) -
-With uv (recommended) - ```sh git clone git@github.com:thereisnotime/hackerone-cli.git cd hackerone-cli -uv sync +uv tool install -e . ``` -
+This installs `hackerone` globally so you can run it from anywhere. Alternatively:
-With pip +Other install methods + +**With pip:** ```sh -git clone git@github.com:thereisnotime/hackerone-cli.git -cd hackerone-cli pip install -e . ``` -
+**With uv (project-local):** -
-With just +```sh +uv sync # install in .venv +uv run hackerone programs # run via uv +# or activate the venv directly: +source .venv/bin/activate +hackerone programs +``` + +**With just:** ```sh just install ``` -Run `just` to see all available recipes. -
## Configuration @@ -95,9 +97,7 @@ Get your API key from [HackerOne Settings](https://hackerone.com/settings/api_to ## Usage ```sh -uv run hackerone [args] [options] # with uv -hackerone [args] [options] # with pip -just run [args] # with just +hackerone [args] [options] ``` ### Global Options From 193cdde9f2e0709f895c81ec4f454f0486593e8f Mon Sep 17 00:00:00 2001 From: thereisnotime <37583483+thereisnotime@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:21:09 +0200 Subject: [PATCH 06/12] Make mdv optional, add verbose mode, fix credential handling - mdv is now an optional dependency (install with .[markdown]) - Lazy-import mdv only when rendering markdown, no more import-time hangs - Add --verbose / -v flag for progress logs (stderr) - Defer .env loading to main() so env var overrides work properly - Treat empty credential strings as missing - help and scope no longer require credentials - Add update/uninstall instructions to README - Bump version to 1.0.2 --- README.md | 45 ++++++++++++++------- hackerone.py | 106 +++++++++++++++++++++++++++++++------------------ justfile | 6 +++ pyproject.toml | 6 ++- uv.lock | 11 +++-- 5 files changed, 116 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index f67012d..2e5a1ff 100644 --- a/README.md +++ b/README.md @@ -6,21 +6,16 @@ An unofficial CLI client for [HackerOne](https://hackerone.com/). Manage your pr - [Quick Start](#quick-start) - [Installation](#installation) + - [Update](#update) | [Uninstall](#uninstall) - [Configuration](#configuration) - - [Credentials](#credentials) - - [Custom .env File](#custom-env-file) + - [Credentials](#credentials) | [Custom .env File](#custom-env-file) - [Usage](#usage) - - [Global Options](#global-options) - - [JSON Output](#json-output) + - [Global Options](#global-options) | [JSON Output](#json-output) - [Commands](#commands) - - [Programs & Scope](#programs--scope) - - [`programs`](#programs-max) | [`program`](#program-handle) | [`csv`](#csv-handle) | [`scope`](#scope-csv-outfile) | [`burp`](#burp-handle) - - [Reports](#reports) - - [`reports`](#reports-1) | [`report`](#report-id) - - [Payments](#payments) - - [`balance`](#balance) | [`earnings`](#earnings) | [`payouts`](#payouts) - - [Account](#account) - - [`profile`](#profile) | [`help`](#help) + - [Programs & Scope](#programs--scope) — `programs` `program` `csv` `scope` `burp` + - [Reports](#reports) — `reports` `report` + - [Payments](#payments) — `balance` `earnings` `payouts` + - [Account](#account) — `profile` `help` - [Development](#development) - [License](#license) @@ -45,10 +40,32 @@ cd hackerone-cli uv tool install -e . ``` -This installs `hackerone` globally so you can run it from anywhere. Alternatively: +For rendered markdown in `report` and `program` output, install with the optional `markdown` extra: + +```sh +uv tool install -e ".[markdown]" +``` + +This installs `hackerone` globally so you can run it from anywhere. + +### Update + +```sh +cd hackerone-cli +git pull +uv tool install -e . --force +``` + +### Uninstall + +```sh +uv tool uninstall hackerone-cli +``` + +### Other install methods
-Other install methods +Click to expand **With pip:** diff --git a/hackerone.py b/hackerone.py index fdb8b03..406bab6 100644 --- a/hackerone.py +++ b/hackerone.py @@ -6,26 +6,33 @@ import re import sys -# Set COLUMNS before importing mdv to suppress its "Could not derive terminal width" warning -if "COLUMNS" not in os.environ: - try: - os.environ["COLUMNS"] = str(os.get_terminal_size()[0]) - except OSError: - os.environ["COLUMNS"] = "80" - -import mdv # noqa: E402 import requests from dotenv import load_dotenv from requests.auth import HTTPBasicAuth -__version__ = "1.0.1" - -load_dotenv() +__version__ = "1.0.2" JSON_OUTPUT = False +VERBOSE = False auth = None +def _render_markdown(text): + """Try to render markdown with mdv, fall back to plain text.""" + try: + if "COLUMNS" not in os.environ: + try: + os.environ["COLUMNS"] = str(os.get_terminal_size()[0]) + except OSError: + os.environ["COLUMNS"] = "80" + import mdv + + mdv.term_columns = _get_terminal_width() + return mdv.main(text) + except Exception: + return text + + def _get_terminal_width(default=80): try: return os.get_terminal_size()[0] @@ -33,6 +40,11 @@ def _get_terminal_width(default=80): return default +def _log(msg): + if VERBOSE and not JSON_OUTPUT: + print(f"[*] {msg}", file=sys.stderr) + + def _error(msg): if JSON_OUTPUT: print(json.dumps({"error": msg})) @@ -42,7 +54,7 @@ def _error(msg): def _error_exit(msg): _error(msg) - sys.exit() + sys.exit(1) def show_help(): @@ -71,6 +83,7 @@ def show_help(): print(" --api-key, -k HackerOne API key (overrides HACKERONE_API_KEY)") print(" --json, -j Output as JSON") print(" --env-file Path to .env file (default: .env in current directory)") + print(" --verbose, -v Show progress and debug info") def burp(): @@ -80,6 +93,7 @@ def burp(): handler = sys.argv[2] + _log(f"Downloading Burp config for '{handler}'...") r = requests.get(f"https://hackerone.com/teams/{handler}/assets/download_burp_project_file.json") if r.status_code != 200 and r.status_code != 404: _error_exit(f"Request returned {r.status_code}!") @@ -104,6 +118,7 @@ def csv(): handler = sys.argv[2] + _log(f"Downloading CSV scope for '{handler}'...") r = requests.get(f"https://hackerone.com/teams/{handler}/assets/download_csv.csv") if r.status_code != 200 and r.status_code != 404: _error_exit(f"Request returned {r.status_code}!") @@ -122,6 +137,7 @@ def csv(): def reports(): + _log("Fetching reports...") r = requests.get("https://api.hackerone.com/v1/hackers/me/reports", auth=auth) if r.status_code != 200: _error_exit(f"Request returned {r.status_code}!") @@ -173,6 +189,7 @@ def report(): id = sys.argv[2] + _log(f"Fetching report {id}...") r = requests.get(f"https://api.hackerone.com/v1/hackers/reports/{id}", auth=auth) if r.status_code != 200 and r.status_code != 404: _error_exit(f"Request returned {r.status_code}!") @@ -225,11 +242,7 @@ def report(): if "vulnerability_information" in rep["attributes"]: print("\nContent") print("--------------------") - try: - mdv.term_columns = _get_terminal_width() - print(mdv.main(rep["attributes"]["vulnerability_information"])) - except FileNotFoundError: - print(rep["attributes"]["vulnerability_information"]) + print(_render_markdown(rep["attributes"]["vulnerability_information"])) print("\nComments") for comment in rep["relationships"]["activities"]["data"]: @@ -380,6 +393,7 @@ def report(): def balance(): + _log("Fetching balance...") r = requests.get("https://api.hackerone.com/v1/hackers/payments/balance", auth=auth) if r.status_code != 200: _error_exit(f"Request returned {r.status_code}!") @@ -393,6 +407,7 @@ def balance(): def earnings(): + _log("Fetching earnings...") r = requests.get("https://api.hackerone.com/v1/hackers/payments/earnings", auth=auth) if r.status_code != 200: _error_exit(f"Request returned {r.status_code}!") @@ -425,6 +440,7 @@ def earnings(): def payouts(): + _log("Fetching payouts...") r = requests.get("https://api.hackerone.com/v1/hackers/payments/payouts", auth=auth) if r.status_code != 200: _error_exit(f"Request returned {r.status_code}!") @@ -461,7 +477,9 @@ def programs(): results = [] + _log("Fetching programs...") while True: + _log(f" Fetching page {c + 1}...") r = requests.get(f"https://api.hackerone.com/v1/hackers/programs?page[size]=100&page[number]={c}", auth=auth) if r.status_code != 200 and r.status_code != 404: _error_exit(f"Request returned {r.status_code}!") @@ -478,6 +496,7 @@ def programs(): c += 1 + _log(f"Fetched {len(results)} programs total.") results = results[::-1] results = results[:limit] @@ -505,6 +524,7 @@ def programs(): def profile(): + _log("Fetching profile...") r = requests.get("https://api.hackerone.com/v1/hackers/me/reports", auth=auth) if r.status_code != 200: _error_exit(f"Request returned {r.status_code}!") @@ -560,6 +580,7 @@ def program(): return handle = sys.argv[2] + _log(f"Fetching program '{handle}'...") r = requests.get(f"https://api.hackerone.com/v1/hackers/programs/{handle}", auth=auth) if r.status_code != 200: _error_exit(f"Request returned {r.status_code}!") @@ -582,11 +603,7 @@ def program(): print("\nPolicy") print("--------------------") - try: - mdv.term_columns = _get_terminal_width() - print(mdv.main(data["attributes"]["policy"])) - except FileNotFoundError: - print(data["attributes"]["policy"]) + print(_render_markdown(data["attributes"]["policy"])) print("\nScope") for scope in data["relationships"]["structured_scopes"]["data"]: @@ -669,36 +686,47 @@ def _extract_flag(flag, *aliases): return None +# Commands that don't need authentication +NO_AUTH_COMMANDS = {"help", "scope"} + + def main(): - global JSON_OUTPUT, auth + global JSON_OUTPUT, VERBOSE, auth if "--json" in sys.argv or "-j" in sys.argv: JSON_OUTPUT = True sys.argv = [a for a in sys.argv if a not in ("--json", "-j")] + if "--verbose" in sys.argv or "-v" in sys.argv: + VERBOSE = True + sys.argv = [a for a in sys.argv if a not in ("--verbose", "-v")] + env_file = _extract_flag("--env-file") - if env_file: - load_dotenv(env_file, override=True) + load_dotenv(env_file if env_file else ".env") - username = _extract_flag("--username", "-u") or os.getenv("HACKERONE_USERNAME") - api_key = _extract_flag("--api-key", "-k") or os.getenv("HACKERONE_API_KEY") + username = _extract_flag("--username", "-u") or os.getenv("HACKERONE_USERNAME") or None + api_key = _extract_flag("--api-key", "-k") or os.getenv("HACKERONE_API_KEY") or None if not JSON_OUTPUT: print() - if username is None: - _error_exit("No username provided! Use --username or set HACKERONE_USERNAME.") - if api_key is None: - _error_exit("No API key provided! Use --api-key or set HACKERONE_API_KEY.") - - auth = HTTPBasicAuth(username, api_key) - if len(sys.argv) < 2: _error("No argument provided!") if not JSON_OUTPUT: print(f"Usage: {__file__} help") - sys.exit() + sys.exit(1) + + command = sys.argv[1] + + # Only require credentials for commands that hit the API + if command not in NO_AUTH_COMMANDS: + if username is None: + _error_exit("No username provided! Use --username or set HACKERONE_USERNAME.") + if api_key is None: + _error_exit("No API key provided! Use --api-key or set HACKERONE_API_KEY.") + auth = HTTPBasicAuth(username, api_key) + _log(f"Authenticated as '{username}'.") - match sys.argv[1]: + match command: case "csv": csv() case "help": @@ -724,12 +752,12 @@ def main(): case "scope": scope() case _: - _error(f"Invalid module '{sys.argv[1]}'") - sys.exit() + _error(f"Invalid module '{command}'") + sys.exit(1) if __name__ == "__main__": try: main() except KeyboardInterrupt: - print("Exiting...") + print("\nExiting...") diff --git a/justfile b/justfile index 9f856d3..f28654a 100644 --- a/justfile +++ b/justfile @@ -40,10 +40,16 @@ test: uv run hackerone help > /dev/null echo "Testing help --json..." uv run hackerone help --json | python3 -m json.tool > /dev/null + echo "Testing no-creds error..." + output=$(HACKERONE_USERNAME="" HACKERONE_API_KEY="" uv run hackerone --env-file /dev/null balance 2>&1 || true) + echo "$output" | grep -q "No username provided" && echo " OK" || { echo " FAIL: should error without creds"; exit 1; } echo "Testing balance --json..." uv run hackerone balance --json | python3 -m json.tool > /dev/null echo "Testing programs 1 --json..." uv run hackerone programs 1 --json | python3 -m json.tool > /dev/null + echo "Testing verbose..." + output=$(uv run hackerone balance -v 2>&1) + echo "$output" | grep -q "Authenticated" && echo " OK" || { echo " FAIL: verbose should show auth info"; exit 1; } echo "All tests passed." # Clean up generated files diff --git a/pyproject.toml b/pyproject.toml index 157102c..813fff3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "hackerone-cli" -version = "1.0.1" +version = "1.0.2" description = "An unofficial CLI tool for interacting with the HackerOne API" readme = "README.md" license = { text = "MIT" } @@ -8,9 +8,11 @@ requires-python = ">=3.10" dependencies = [ "requests", "python-dotenv", - "mdv", ] +[project.optional-dependencies] +markdown = ["mdv"] + [project.scripts] hackerone = "hackerone:main" diff --git a/uv.lock b/uv.lock index cbb7b09..53478aa 100644 --- a/uv.lock +++ b/uv.lock @@ -118,14 +118,18 @@ wheels = [ [[package]] name = "hackerone-cli" -version = "1.0.1" +version = "1.0.2" source = { editable = "." } dependencies = [ - { name = "mdv" }, { name = "python-dotenv" }, { name = "requests" }, ] +[package.optional-dependencies] +markdown = [ + { name = "mdv" }, +] + [package.dev-dependencies] dev = [ { name = "ruff" }, @@ -133,10 +137,11 @@ dev = [ [package.metadata] requires-dist = [ - { name = "mdv" }, + { name = "mdv", marker = "extra == 'markdown'" }, { name = "python-dotenv" }, { name = "requests" }, ] +provides-extras = ["markdown"] [package.metadata.requires-dev] dev = [{ name = "ruff", specifier = ">=0.15.7" }] From 2f84d5f7aa080a876fe69af14269b7b3f99b7758 Mon Sep 17 00:00:00 2001 From: thereisnotime <37583483+thereisnotime@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:33:44 +0200 Subject: [PATCH 07/12] Add program management commands (org, org-reports, org-report) Support the "other side" of HackerOne - program managers can now list reports submitted to their programs, view report details, and check org info. These use the /v1/reports and /v1/me/organizations endpoints and require an organization-level API token. Help output now separates hacker and program management commands. --- README.md | 65 +++++++++++++++- hackerone.py | 215 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 266 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 2e5a1ff..3c91377 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,8 @@ An unofficial CLI client for [HackerOne](https://hackerone.com/). Manage your pr - [Usage](#usage) - [Global Options](#global-options) | [JSON Output](#json-output) - [Commands](#commands) - - [Programs & Scope](#programs--scope) — `programs` `program` `csv` `scope` `burp` - - [Reports](#reports) — `reports` `report` - - [Payments](#payments) — `balance` `earnings` `payouts` - - [Account](#account) — `profile` `help` + - Hacker: [Programs & Scope](#programs--scope) | [Reports](#reports) | [Payments](#payments) | [Account](#account) + - Program Manager: [Organization](#organization--program-management) - [Development](#development) - [License](#license) @@ -125,6 +123,7 @@ hackerone [args] [options] | `--username ` | `-u` | HackerOne username (overrides env) | | `--api-key ` | `-k` | HackerOne API key (overrides env) | | `--env-file ` | | Path to a custom `.env` file | +| `--verbose` | `-v` | Show progress and debug info | ### JSON Output @@ -153,6 +152,10 @@ hackerone program security -j | jq '.attributes.handle' | [`payouts`](#payouts) | Payout history | | [`profile`](#profile) | Your profile info | | [`help`](#help) | Show available commands | +| | **Program Management** (requires org API token) | +| [`org`](#org) | Show your organization info | +| [`org-reports [max]`](#org-reports-handle-max) | List reports submitted to your program | +| [`org-report `](#org-report-id) | Get a report submitted to your program | --- @@ -417,6 +420,60 @@ Location: Right here Show all available commands and their arguments. +--- + +### Organization / Program Management + +These commands use the **program management API** and require an organization-level API token (created in Organization Settings > API Tokens on HackerOne), not a hacker profile token. + +You can use both token types in the same `.env` by switching with `--username` / `--api-key` flags, or use separate `.env` files with `--env-file`. + +#### `org` + +Show your organization info — ID, handle, name, and permissions. + +``` +$ hackerone org + +Organization +---------------------------------------- +ID: 12345 +Handle: mycompany +Name: My Company +Created: 2025-01-01T00:00:00.000Z +Permissions: report_management, reward +---------------------------------------- +``` + +#### `org-reports [max]` + +List reports submitted to your program. Defaults to 10 results. Shows reporter, severity, state, and more. + +``` +$ hackerone org-reports mycompany 5 + +Reports for 'mycompany' +---------------------------------------- +ID: 1234567 +Title: XSS on login page +State: triaged +Date: 2027-03-13T16:48:17.286Z +Reporter: hackerman +Severity: high +CVSS: 8.3 +---------------------------------------- + +Showing 1 of 1 results. +``` + +#### `org-report ` + +Full details on a report submitted to your program, including content, activities, and metadata. + +``` +$ hackerone org-report 1234567 +``` + ## Development ```sh diff --git a/hackerone.py b/hackerone.py index 406bab6..0ac61f9 100644 --- a/hackerone.py +++ b/hackerone.py @@ -58,7 +58,7 @@ def _error_exit(msg): def show_help(): - commands = { + hacker_commands = { "balance": "Check your balance", "burp ": "Download the burp configuration file (only from public programs)", "csv ": "Download CSV scope file (only from public programs)", @@ -72,18 +72,27 @@ def show_help(): "reports": "Get a list of your reports", "scope []": "Extract in-scope domains/URLs/wildcards/IPs/CIDRs from a csv scope file and save it to a text file", } + org_commands = { + "org": "Show your organization info", + "org-reports []": "List reports submitted to your program (default: 10)", + "org-report ": "Get a report submitted to your program", + } + all_commands = {**hacker_commands, **org_commands} if JSON_OUTPUT: - print(json.dumps({"commands": commands})) + print(json.dumps({"commands": all_commands})) return - print("Modules:") - for cmd, desc in commands.items(): - print(f" {cmd:<32}{desc}") + print("Hacker Modules:") + for cmd, desc in hacker_commands.items(): + print(f" {cmd:<36}{desc}") + print("\nProgram Management Modules:") + for cmd, desc in org_commands.items(): + print(f" {cmd:<36}{desc}") print("\nOptions:") - print(" --username, -u HackerOne username (overrides HACKERONE_USERNAME)") - print(" --api-key, -k HackerOne API key (overrides HACKERONE_API_KEY)") - print(" --json, -j Output as JSON") - print(" --env-file Path to .env file (default: .env in current directory)") - print(" --verbose, -v Show progress and debug info") + print(" --username, -u HackerOne API token identifier (overrides HACKERONE_USERNAME)") + print(" --api-key, -k HackerOne API token value (overrides HACKERONE_API_KEY)") + print(" --json, -j Output as JSON") + print(" --env-file Path to .env file (default: .env in current directory)") + print(" --verbose, -v Show progress and debug info") def burp(): @@ -671,6 +680,186 @@ def scope(): return +# --- Program Management (Organization) Commands --- + + +def org(): + _log("Fetching organization info...") + r = requests.get("https://api.hackerone.com/v1/me/organizations", auth=auth) + if r.status_code != 200: + _error_exit(f"Request returned {r.status_code}!") + data = json.loads(r.text) + + if JSON_OUTPUT: + print(json.dumps(data)) + return + + if len(data["data"]) == 0: + print("You don't belong to any organization.") + return + + for o in data["data"]: + print("Organization") + print("----------------------------------------") + print("ID: " + o["id"]) + print("Handle: " + o["attributes"]["handle"]) + print("Name: " + o["attributes"]["name"]) + print("Created: " + o["attributes"]["created_at"]) + if "permissions" in o["attributes"] and o["attributes"]["permissions"]: + print("Permissions: " + ", ".join(o["attributes"]["permissions"])) + print("----------------------------------------") + + +def org_reports(): + if len(sys.argv) < 3: + _error("No program handle provided!") + return + + handle = sys.argv[2] + limit = 10 + if len(sys.argv) >= 4: + if sys.argv[3].isdigit() and int(sys.argv[3]) > 0: + limit = int(sys.argv[3]) + else: + _error(f"Invalid maximum value '{sys.argv[3]}'!") + return + + _log(f"Fetching reports for program '{handle}'...") + params = { + "filter[program][]": handle, + "page[size]": min(limit, 100), + "page[number]": 1, + } + r = requests.get("https://api.hackerone.com/v1/reports", params=params, auth=auth) + if r.status_code != 200: + _error_exit(f"Request returned {r.status_code}!") + data = json.loads(r.text) + + if JSON_OUTPUT: + print(json.dumps(data)) + return + + if len(data["data"]) == 0: + print(f"No reports found for program '{handle}'.") + return + + print(f"Reports for '{handle}'") + print("----------------------------------------") + count = 0 + for rep in data["data"]: + if count >= limit: + break + print("ID: " + rep["id"]) + print("Title: " + rep["attributes"]["title"]) + print("State: " + rep["attributes"]["state"]) + print("Date: " + rep["attributes"]["created_at"]) + try: + print("Reporter: " + rep["relationships"]["reporter"]["data"]["attributes"]["username"]) + except: + print("Reporter: unknown") + try: + print("Severity: " + rep["relationships"]["severity"]["data"]["attributes"]["rating"]) + except: + print("Severity: none") + try: + print("CWE: " + str.upper(rep["relationships"]["weakness"]["data"]["attributes"]["external_id"])) + except: + pass + try: + print("CVSS: " + str(rep["relationships"]["severity"]["data"]["attributes"]["score"])) + except: + pass + print("----------------------------------------") + count += 1 + print(f"\nShowing {count} of {len(data['data'])} results.") + + +def org_report(): + if len(sys.argv) != 3: + _error("Invalid arguments provided!") + return + + if not sys.argv[2].isdigit(): + _error(f"Invalid ID provided '{sys.argv[2]}'!") + return + + id = sys.argv[2] + + _log(f"Fetching report {id}...") + r = requests.get(f"https://api.hackerone.com/v1/reports/{id}", auth=auth) + if r.status_code == 404: + _error("Report not found!") + return + if r.status_code != 200: + _error_exit(f"Request returned {r.status_code}!") + data = json.loads(r.text) + + if JSON_OUTPUT: + print(json.dumps(data)) + return + + rep = data["data"] + + print("Report") + print("----------------------------------------") + print("ID: " + rep["id"]) + print("Title: " + rep["attributes"]["title"]) + print("State: " + rep["attributes"]["state"]) + print("Date: " + rep["attributes"]["created_at"]) + try: + print("Reporter: " + rep["relationships"]["reporter"]["data"]["attributes"]["username"]) + except: + print("Reporter: unknown") + try: + print("Program: " + rep["relationships"]["program"]["data"]["attributes"]["handle"]) + except: + pass + try: + print("Severity: " + rep["relationships"]["severity"]["data"]["attributes"]["rating"]) + except: + print("Severity: none") + if "cve_ids" in rep["attributes"] and rep["attributes"]["cve_ids"] not in [None, "", []]: + print("CVE: " + ", ".join(rep["attributes"]["cve_ids"])) + try: + print("CWE: " + str.upper(rep["relationships"]["weakness"]["data"]["attributes"]["external_id"])) + print("Weakness: " + rep["relationships"]["weakness"]["data"]["attributes"]["name"]) + except: + pass + try: + print("Asset: " + rep["relationships"]["structured_scope"]["data"]["attributes"]["asset_identifier"]) + except: + pass + try: + print("CVSS: " + str(rep["relationships"]["severity"]["data"]["attributes"]["score"])) + except: + pass + + if "vulnerability_information" in rep["attributes"] and rep["attributes"]["vulnerability_information"]: + print("\nContent") + print("--------------------") + print(_render_markdown(rep["attributes"]["vulnerability_information"])) + + try: + activities = rep["relationships"]["activities"]["data"] + if activities: + print("\nActivities") + for act in activities: + print("--------------------") + try: + actor = act["relationships"]["actor"]["data"]["attributes"] + entity = actor.get("username") or actor.get("handle") or "someone" + except: + entity = "someone" + act_type = act["type"].replace("activity-", "").replace("-", " ").title() + msg = "" + if "message" in act.get("attributes", {}) and act["attributes"]["message"]: + msg = "\n" + act["attributes"]["message"] + print(f"@{entity} — {act_type}{msg}") + print("----------------------------------------") + except: + pass + + def _extract_flag(flag, *aliases): """Extract a --flag value from sys.argv, removing both the flag and its value.""" for name in (flag, *aliases): @@ -751,6 +940,12 @@ def main(): burp() case "scope": scope() + case "org": + org() + case "org-reports": + org_reports() + case "org-report": + org_report() case _: _error(f"Invalid module '{command}'") sys.exit(1) From 2e691885cef19d43579d21aa07968fedc2cf70b4 Mon Sep 17 00:00:00 2001 From: thereisnotime <37583483+thereisnotime@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:48:18 +0200 Subject: [PATCH 08/12] Replace lint workflow with full CI pipeline - Lint job: ruff check + format - Test job: matrix across Linux/macOS/Windows and Python 3.10/3.12/3.13 - Tests: help, JSON validation, error handling, no-creds behavior - Add CI badge to README --- .github/workflows/ci.yml | 80 ++++++++++++++++++++++++++++++++++++++ .github/workflows/lint.yml | 28 ------------- README.md | 2 + 3 files changed, 82 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fb14e7e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Set up Python + run: uv python install 3.10 + + - name: Install dependencies + run: uv sync + + - name: Ruff check + run: uv run ruff check . + + - name: Ruff format check + run: uv run ruff format --check . + + test: + name: Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python: ["3.10", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Set up Python + run: uv python install ${{ matrix.python }} + + - name: Install dependencies + run: uv sync + + - name: Test help + run: uv run hackerone help + + - name: Test help JSON + run: uv run hackerone help --json + + - name: Test help JSON is valid + run: uv run hackerone help --json | python3 -m json.tool > /dev/null + + - name: Test no-creds error + shell: bash + run: | + output=$(HACKERONE_USERNAME="" HACKERONE_API_KEY="" uv run hackerone --env-file /dev/null balance 2>&1 || true) + echo "$output" | grep -q "No username provided" + + - name: Test no-args error + shell: bash + run: | + output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" uv run hackerone --env-file /dev/null 2>&1 || true) + echo "$output" | grep -q "No argument provided" + + - name: Test invalid module error + shell: bash + run: | + output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" uv run hackerone --env-file /dev/null notamodule 2>&1 || true) + echo "$output" | grep -q "Invalid module" + + - name: Test help without creds + shell: bash + run: | + HACKERONE_USERNAME="" HACKERONE_API_KEY="" uv run hackerone --env-file /dev/null help diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index c97b385..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Lint - -on: - push: - branches: [main, master] - pull_request: - branches: [main, master] - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v4 - - - name: Set up Python - run: uv python install 3.10 - - - name: Install dependencies - run: uv sync - - - name: Ruff check - run: uv run ruff check . - - - name: Ruff format check - run: uv run ruff format --check . diff --git a/README.md b/README.md index 3c91377..9a2184c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # HackerOne CLI +[![CI](https://github.com/thereisnotime/hackerone-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/thereisnotime/hackerone-cli/actions/workflows/ci.yml) + An unofficial CLI client for [HackerOne](https://hackerone.com/). Manage your profile, reports, programs, payments, and more from the terminal. Built on the official [HackerOne API v1](https://api.hackerone.com/). ## Table of Contents From 0171c15bb1aac34f5660091806654391627d82e0 Mon Sep 17 00:00:00 2001 From: thereisnotime <37583483+thereisnotime@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:51:14 +0200 Subject: [PATCH 09/12] Fix CI: use bash shell on all test steps for Windows compat --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb14e7e..82ae384 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,12 +48,15 @@ jobs: run: uv sync - name: Test help + shell: bash run: uv run hackerone help - name: Test help JSON + shell: bash run: uv run hackerone help --json - name: Test help JSON is valid + shell: bash run: uv run hackerone help --json | python3 -m json.tool > /dev/null - name: Test no-creds error From aca9fdc7f669e83d0caba3cf78bda109438b335a Mon Sep 17 00:00:00 2001 From: thereisnotime <37583483+thereisnotime@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:59:22 +0200 Subject: [PATCH 10/12] Add all missing program management API endpoints New commands: org-members, org-groups, org-invitations, org-update-report, org-activities, org-metrics, org-scopes, org-invite-hacker, org-bounty, org-swag. Full coverage of the HackerOne program management API. --- README.md | 96 ++++++++++- hackerone.py | 449 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 542 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9a2184c..c7bf266 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ An unofficial CLI client for [HackerOne](https://hackerone.com/). Manage your pr - [Global Options](#global-options) | [JSON Output](#json-output) - [Commands](#commands) - Hacker: [Programs & Scope](#programs--scope) | [Reports](#reports) | [Payments](#payments) | [Account](#account) - - Program Manager: [Organization](#organization--program-management) + - Program Manager: [Organization](#organization--program-management) — `org` `org-reports` `org-update-report` `org-metrics` `org-bounty` and more - [Development](#development) - [License](#license) @@ -156,8 +156,18 @@ hackerone program security -j | jq '.attributes.handle' | [`help`](#help) | Show available commands | | | **Program Management** (requires org API token) | | [`org`](#org) | Show your organization info | +| [`org-members `](#org-members-org_id) | List organization members | +| [`org-groups `](#org-groups-org_id) | List permission groups | +| [`org-invitations `](#org-invitations-org_id) | List pending invitations | | [`org-reports [max]`](#org-reports-handle-max) | List reports submitted to your program | | [`org-report `](#org-report-id) | Get a report submitted to your program | +| [`org-update-report `](#org-update-report-id-state) | Update report state | +| [`org-activities [handle]`](#org-activities-handle) | List recent activities | +| [`org-metrics `](#org-metrics-handle) | Program metrics | +| [`org-scopes `](#org-scopes-handle) | List program scope/assets | +| [`org-invite-hacker `](#org-invite-hacker-program_id-username) | Invite hacker to program | +| [`org-bounty `](#org-bounty-report_id-amount) | Award bounty | +| [`org-swag `](#org-swag-report_id) | Award swag | --- @@ -476,6 +486,90 @@ Full details on a report submitted to your program, including content, activitie $ hackerone org-report 1234567 ``` +#### `org-update-report ` + +Update the state of a report. Valid states: `triaged`, `resolved`, `duplicate`, `spam`, `not-applicable`, `informative`, `needs-more-info`, `new`. + +```sh +hackerone org-update-report 1234567 triaged +hackerone org-update-report 1234567 resolved "Fixed in v2.1" +``` + +#### `org-members ` + +List all members in your organization. Get your org ID from `org`. + +```sh +hackerone org-members 12345 +``` + +#### `org-groups ` + +List permission groups in your organization. + +```sh +hackerone org-groups 12345 +``` + +#### `org-invitations ` + +List pending organization member invitations. + +```sh +hackerone org-invitations 12345 +``` + +#### `org-activities [handle]` + +List recent activities across your programs. Optionally filter by program handle. + +```sh +hackerone org-activities +hackerone org-activities mycompany +``` + +#### `org-metrics ` + +Get program health metrics — response times, acceptance rates, efficiency stats. + +```sh +hackerone org-metrics mycompany +``` + +#### `org-scopes ` + +List all structured scopes (assets) for a program, grouped by in-scope and out-of-scope. + +```sh +hackerone org-scopes mycompany +``` + +#### `org-invite-hacker ` + +Invite a hacker to a private program. + +```sh +hackerone org-invite-hacker 12345 hackerman +``` + +#### `org-bounty ` + +Award a bounty on a report. Optionally include a message. + +```sh +hackerone org-bounty 1234567 500 +hackerone org-bounty 1234567 1000 "Great find, thanks!" +``` + +#### `org-swag ` + +Award swag on a report. Optionally include a message. + +```sh +hackerone org-swag 1234567 +hackerone org-swag 1234567 "T-shirt on its way!" +``` + ## Development ```sh diff --git a/hackerone.py b/hackerone.py index 0ac61f9..21a171a 100644 --- a/hackerone.py +++ b/hackerone.py @@ -74,8 +74,18 @@ def show_help(): } org_commands = { "org": "Show your organization info", + "org-members ": "List organization members", + "org-groups ": "List organization permission groups", + "org-invitations ": "List pending organization invitations", "org-reports []": "List reports submitted to your program (default: 10)", "org-report ": "Get a report submitted to your program", + "org-update-report ": "Update report state (triaged, resolved, duplicate, spam, not-applicable)", + "org-activities []": "List recent activities (optionally filter by program)", + "org-metrics ": "Get program metrics and response efficiency", + "org-scopes ": "List structured scopes (assets) for a program", + "org-invite-hacker ": "Invite a hacker to a private program", + "org-bounty ": "Award a bounty on a report", + "org-swag ": "Award swag on a report", } all_commands = {**hacker_commands, **org_commands} if JSON_OUTPUT: @@ -83,10 +93,10 @@ def show_help(): return print("Hacker Modules:") for cmd, desc in hacker_commands.items(): - print(f" {cmd:<36}{desc}") + print(f" {cmd:<44}{desc}") print("\nProgram Management Modules:") for cmd, desc in org_commands.items(): - print(f" {cmd:<36}{desc}") + print(f" {cmd:<44}{desc}") print("\nOptions:") print(" --username, -u HackerOne API token identifier (overrides HACKERONE_USERNAME)") print(" --api-key, -k HackerOne API token value (overrides HACKERONE_API_KEY)") @@ -860,6 +870,421 @@ def org_report(): pass +def org_members(): + if len(sys.argv) < 3: + _error("No organization ID provided! Use 'org' to find your org ID.") + return + + org_id = sys.argv[2] + _log(f"Fetching members for organization {org_id}...") + r = requests.get(f"https://api.hackerone.com/v1/organizations/{org_id}/members", auth=auth) + if r.status_code != 200: + _error_exit(f"Request returned {r.status_code}!") + data = json.loads(r.text) + + if JSON_OUTPUT: + print(json.dumps(data)) + return + + if len(data["data"]) == 0: + print("No members found.") + return + + print("Members") + print("----------------------------------------") + for member in data["data"]: + try: + user = member["relationships"]["user"]["data"]["attributes"] + print("Username: " + user.get("username", "unknown")) + except: + print("ID: " + member["id"]) + try: + groups = member["relationships"]["organization_member_groups"]["data"] + group_names = [g["attributes"]["name"] for g in groups] + if group_names: + print("Groups: " + ", ".join(group_names)) + except: + pass + print("----------------------------------------") + print(f"\n{len(data['data'])} members.") + + +def org_groups(): + if len(sys.argv) < 3: + _error("No organization ID provided! Use 'org' to find your org ID.") + return + + org_id = sys.argv[2] + _log(f"Fetching groups for organization {org_id}...") + r = requests.get(f"https://api.hackerone.com/v1/organizations/{org_id}/groups", auth=auth) + if r.status_code != 200: + _error_exit(f"Request returned {r.status_code}!") + data = json.loads(r.text) + + if JSON_OUTPUT: + print(json.dumps(data)) + return + + if len(data["data"]) == 0: + print("No groups found.") + return + + print("Groups") + print("----------------------------------------") + for group in data["data"]: + print("ID: " + group["id"]) + print("Name: " + group["attributes"]["name"]) + try: + perms = group["attributes"]["permissions"] + if perms: + print("Permissions: " + ", ".join(perms)) + except: + pass + print("----------------------------------------") + + +def org_invitations(): + if len(sys.argv) < 3: + _error("No organization ID provided! Use 'org' to find your org ID.") + return + + org_id = sys.argv[2] + _log(f"Fetching invitations for organization {org_id}...") + r = requests.get(f"https://api.hackerone.com/v1/organizations/{org_id}/invitations", auth=auth) + if r.status_code != 200: + _error_exit(f"Request returned {r.status_code}!") + data = json.loads(r.text) + + if JSON_OUTPUT: + print(json.dumps(data)) + return + + if len(data["data"]) == 0: + print("No pending invitations.") + return + + print("Pending Invitations") + print("----------------------------------------") + for inv in data["data"]: + print("ID: " + inv["id"]) + try: + print("Email: " + inv["attributes"]["email"]) + except: + pass + try: + print("Created: " + inv["attributes"]["created_at"]) + except: + pass + print("----------------------------------------") + + +def org_update_report(): + if len(sys.argv) < 4: + _error("Usage: org-update-report ") + _error("States: triaged, resolved, duplicate, spam, not-applicable") + return + + if not sys.argv[2].isdigit(): + _error(f"Invalid report ID '{sys.argv[2]}'!") + return + + report_id = sys.argv[2] + state = sys.argv[3] + + state_map = { + "triaged": "activity-bug-triaged", + "resolved": "activity-bug-resolved", + "duplicate": "activity-bug-duplicate", + "spam": "activity-bug-spam", + "not-applicable": "activity-bug-not-applicable", + "informative": "activity-bug-informative", + "needs-more-info": "activity-bug-needs-more-info", + "new": "activity-bug-new", + } + + if state not in state_map: + _error(f"Invalid state '{state}'. Valid states: {', '.join(state_map.keys())}") + return + + message = sys.argv[4] if len(sys.argv) >= 5 else "" + + _log(f"Updating report {report_id} to '{state}'...") + payload = { + "data": { + "type": state_map[state], + "attributes": { + "message": message, + }, + } + } + + r = requests.post( + f"https://api.hackerone.com/v1/reports/{report_id}/activities", + json=payload, + auth=auth, + ) + if r.status_code not in (200, 201): + _error_exit(f"Request returned {r.status_code}!") + data = json.loads(r.text) + + if JSON_OUTPUT: + print(json.dumps(data)) + return + + print(f"Report {report_id} updated to '{state}'.") + + +def org_activities(): + handle = sys.argv[2] if len(sys.argv) >= 3 else None + + _log("Fetching activities...") + params = {"page[size]": 25} + if handle: + params["filter[program][]"] = handle + + r = requests.get("https://api.hackerone.com/v1/incremental/activities", params=params, auth=auth) + if r.status_code != 200: + _error_exit(f"Request returned {r.status_code}!") + data = json.loads(r.text) + + if JSON_OUTPUT: + print(json.dumps(data)) + return + + if len(data["data"]) == 0: + print("No activities found.") + return + + print("Activities" + (f" for '{handle}'" if handle else "")) + print("----------------------------------------") + for act in data["data"]: + act_type = act["type"].replace("activity-", "").replace("-", " ").title() + try: + actor = act["relationships"]["actor"]["data"]["attributes"] + entity = actor.get("username") or actor.get("handle") or "someone" + except: + entity = "someone" + date = act.get("attributes", {}).get("created_at", "") + msg = "" + if "message" in act.get("attributes", {}) and act["attributes"]["message"]: + msg = "\n " + act["attributes"]["message"][:200] + print(f"[{date}] @{entity} — {act_type}{msg}") + print("--------------------") + + +def org_metrics(): + if len(sys.argv) < 3: + _error("No program handle provided!") + return + + handle = sys.argv[2] + + # First get program ID from handle + _log(f"Fetching program '{handle}' to get ID...") + r = requests.get(f"https://api.hackerone.com/v1/hackers/programs/{handle}", auth=auth) + if r.status_code != 200: + _error_exit(f"Could not find program '{handle}' (status {r.status_code})!") + program_data = json.loads(r.text) + program_id = program_data.get("id", program_data.get("data", {}).get("id")) + + if not program_id: + _error("Could not determine program ID!") + return + + _log(f"Fetching metrics for program {program_id}...") + r = requests.get(f"https://api.hackerone.com/v1/programs/{program_id}/metrics", auth=auth) + if r.status_code != 200: + _error_exit(f"Request returned {r.status_code}!") + data = json.loads(r.text) + + if JSON_OUTPUT: + print(json.dumps(data)) + return + + print(f"Metrics for '{handle}'") + print("----------------------------------------") + if "data" in data: + for key, value in data["data"].get("attributes", data["data"]).items(): + if key not in ("type", "id"): + print(f"{key.replace('_', ' ').title()}: {value}") + else: + for key, value in data.items(): + print(f"{key.replace('_', ' ').title()}: {value}") + print("----------------------------------------") + + +def org_scopes(): + if len(sys.argv) < 3: + _error("No program handle provided!") + return + + handle = sys.argv[2] + + _log(f"Fetching scopes for program '{handle}'...") + r = requests.get( + f"https://api.hackerone.com/v1/hackers/programs/{handle}/structured_scopes", + params={"page[size]": 100}, + auth=auth, + ) + if r.status_code != 200: + _error_exit(f"Request returned {r.status_code}!") + data = json.loads(r.text) + + if JSON_OUTPUT: + print(json.dumps(data)) + return + + if len(data["data"]) == 0: + print(f"No scopes found for '{handle}'.") + return + + in_scope = [s for s in data["data"] if s["attributes"].get("eligible_for_submission")] + out_scope = [s for s in data["data"] if not s["attributes"].get("eligible_for_submission")] + + print(f"Scopes for '{handle}'") + + if in_scope: + print("\nIn-Scope") + print("----------------------------------------") + for s in in_scope: + attrs = s["attributes"] + bounty = "yes" if attrs.get("eligible_for_bounty") else "no" + print(f" {attrs['asset_identifier']} ({attrs['asset_type']}) — bounty: {bounty}") + if attrs.get("instruction"): + print(f" {attrs['instruction'][:120]}") + print() + + if out_scope: + print("Out-of-Scope") + print("----------------------------------------") + for s in out_scope: + attrs = s["attributes"] + print(f" {attrs['asset_identifier']} ({attrs['asset_type']})") + print() + + print(f"{len(in_scope)} in-scope, {len(out_scope)} out-of-scope.") + + +def org_invite_hacker(): + if len(sys.argv) < 4: + _error("Usage: org-invite-hacker ") + return + + program_id = sys.argv[2] + username = sys.argv[3] + + _log(f"Inviting '{username}' to program {program_id}...") + payload = { + "data": [ + { + "type": "hacker-invitation", + "attributes": { + "username": username, + }, + } + ] + } + + r = requests.post( + f"https://api.hackerone.com/v1/programs/{program_id}/hacker_invitations", + json=payload, + auth=auth, + ) + if r.status_code not in (200, 201): + _error_exit(f"Request returned {r.status_code}!") + data = json.loads(r.text) + + if JSON_OUTPUT: + print(json.dumps(data)) + return + + print(f"Invitation sent to '{username}' for program {program_id}.") + + +def org_bounty(): + if len(sys.argv) < 4: + _error("Usage: org-bounty [message]") + return + + if not sys.argv[2].isdigit(): + _error(f"Invalid report ID '{sys.argv[2]}'!") + return + + report_id = sys.argv[2] + amount = sys.argv[3] + message = sys.argv[4] if len(sys.argv) >= 5 else "" + + try: + float(amount) + except ValueError: + _error(f"Invalid amount '{amount}'!") + return + + _log(f"Awarding ${amount} bounty on report {report_id}...") + payload = { + "data": { + "type": "bounty", + "attributes": { + "amount": float(amount), + "message": message, + }, + } + } + + r = requests.post( + f"https://api.hackerone.com/v1/reports/{report_id}/bounties", + json=payload, + auth=auth, + ) + if r.status_code not in (200, 201): + _error_exit(f"Request returned {r.status_code}!") + data = json.loads(r.text) + + if JSON_OUTPUT: + print(json.dumps(data)) + return + + print(f"Bounty of ${amount} awarded on report {report_id}.") + + +def org_swag(): + if len(sys.argv) < 3: + _error("Usage: org-swag [message]") + return + + if not sys.argv[2].isdigit(): + _error(f"Invalid report ID '{sys.argv[2]}'!") + return + + report_id = sys.argv[2] + message = sys.argv[3] if len(sys.argv) >= 4 else "" + + _log(f"Awarding swag on report {report_id}...") + payload = { + "data": { + "type": "swag", + "attributes": { + "message": message, + }, + } + } + + r = requests.post( + f"https://api.hackerone.com/v1/reports/{report_id}/swag", + json=payload, + auth=auth, + ) + if r.status_code not in (200, 201): + _error_exit(f"Request returned {r.status_code}!") + data = json.loads(r.text) + + if JSON_OUTPUT: + print(json.dumps(data)) + return + + print(f"Swag awarded on report {report_id}.") + + def _extract_flag(flag, *aliases): """Extract a --flag value from sys.argv, removing both the flag and its value.""" for name in (flag, *aliases): @@ -942,10 +1367,30 @@ def main(): scope() case "org": org() + case "org-members": + org_members() + case "org-groups": + org_groups() + case "org-invitations": + org_invitations() case "org-reports": org_reports() case "org-report": org_report() + case "org-update-report": + org_update_report() + case "org-activities": + org_activities() + case "org-metrics": + org_metrics() + case "org-scopes": + org_scopes() + case "org-invite-hacker": + org_invite_hacker() + case "org-bounty": + org_bounty() + case "org-swag": + org_swag() case _: _error(f"Invalid module '{command}'") sys.exit(1) From ae1bfbf2e10cd00ecf1c2906aacd391950b09370 Mon Sep 17 00:00:00 2001 From: thereisnotime <37583483+thereisnotime@users.noreply.github.com> Date: Thu, 26 Mar 2026 22:01:10 +0200 Subject: [PATCH 11/12] Expand CI tests to cover all commands and error paths Tests for: help output, JSON validity, all error messages, argument validation for every org command, scope without creds, verbose flag. 30+ test steps across 9 OS/Python combinations. --- .github/workflows/ci.yml | 158 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 148 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82ae384..5460ec8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,10 @@ on: pull_request: branches: [main, master] +env: + NO_CREDS: HACKERONE_USERNAME="" HACKERONE_API_KEY="" + FAKE_CREDS: HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" + jobs: lint: name: Lint @@ -32,6 +36,7 @@ jobs: name: Test runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] python: ["3.10", "3.12", "3.13"] @@ -47,37 +52,170 @@ jobs: - name: Install dependencies run: uv sync - - name: Test help + # --- Help & JSON output --- + + - name: "help: shows output" shell: bash - run: uv run hackerone help + run: uv run hackerone help | grep -q "Hacker Modules" - - name: Test help JSON + - name: "help: works without credentials" shell: bash - run: uv run hackerone help --json + run: | + HACKERONE_USERNAME="" HACKERONE_API_KEY="" uv run hackerone --env-file /dev/null help | grep -q "Hacker Modules" - - name: Test help JSON is valid + - name: "help --json: returns valid JSON" shell: bash run: uv run hackerone help --json | python3 -m json.tool > /dev/null - - name: Test no-creds error + - name: "help --json: contains commands key" + shell: bash + run: | + uv run hackerone help --json | python3 -c "import json,sys; d=json.load(sys.stdin); assert 'commands' in d" + + - name: "help --json: lists all hacker commands" + shell: bash + run: | + uv run hackerone help --json | python3 -c " + import json,sys + cmds = json.load(sys.stdin)['commands'] + for c in ['balance','reports','programs','profile','earnings','payouts']: + assert c in cmds, f'missing {c}' + " + + - name: "help --json: lists all org commands" + shell: bash + run: | + uv run hackerone help --json | python3 -c " + import json,sys + cmds = json.load(sys.stdin)['commands'] + for c in ['org','org-members ','org-reports []','org-bounty ','org-swag ']: + assert c in cmds, f'missing {c}' + " + + # --- Error handling --- + + - name: "error: no credentials" shell: bash run: | output=$(HACKERONE_USERNAME="" HACKERONE_API_KEY="" uv run hackerone --env-file /dev/null balance 2>&1 || true) echo "$output" | grep -q "No username provided" - - name: Test no-args error + - name: "error: no API key" + shell: bash + run: | + output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="" uv run hackerone --env-file /dev/null balance 2>&1 || true) + echo "$output" | grep -q "No API key provided" + + - name: "error: no arguments" shell: bash run: | output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" uv run hackerone --env-file /dev/null 2>&1 || true) echo "$output" | grep -q "No argument provided" - - name: Test invalid module error + - name: "error: invalid module" shell: bash run: | output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" uv run hackerone --env-file /dev/null notamodule 2>&1 || true) echo "$output" | grep -q "Invalid module" - - name: Test help without creds + - name: "error: no-creds JSON outputs valid JSON" + shell: bash + run: | + output=$(HACKERONE_USERNAME="" HACKERONE_API_KEY="" uv run hackerone --env-file /dev/null --json balance 2>&1 || true) + echo "$output" | python3 -c "import json,sys; d=json.load(sys.stdin); assert 'error' in d" + + # --- Argument validation --- + + - name: "report: rejects non-numeric ID" + shell: bash + run: | + output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" uv run hackerone --env-file /dev/null report abc 2>&1 || true) + echo "$output" | grep -q "Invalid ID" + + - name: "org-report: rejects non-numeric ID" + shell: bash + run: | + output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" uv run hackerone --env-file /dev/null org-report abc 2>&1 || true) + echo "$output" | grep -q "Invalid ID" + + - name: "org-update-report: rejects invalid state" + shell: bash + run: | + output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" uv run hackerone --env-file /dev/null org-update-report 123 badstate 2>&1 || true) + echo "$output" | grep -q "Invalid state" + + - name: "org-update-report: rejects missing args" + shell: bash + run: | + output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" uv run hackerone --env-file /dev/null org-update-report 2>&1 || true) + echo "$output" | grep -q "Usage" + + - name: "org-bounty: rejects invalid amount" + shell: bash + run: | + output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" uv run hackerone --env-file /dev/null org-bounty 123 notanumber 2>&1 || true) + echo "$output" | grep -q "Invalid amount" + + - name: "org-bounty: rejects missing args" + shell: bash + run: | + output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" uv run hackerone --env-file /dev/null org-bounty 2>&1 || true) + echo "$output" | grep -q "Usage" + + - name: "org-members: requires org ID" + shell: bash + run: | + output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" uv run hackerone --env-file /dev/null org-members 2>&1 || true) + echo "$output" | grep -q "No organization ID" + + - name: "org-groups: requires org ID" + shell: bash + run: | + output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" uv run hackerone --env-file /dev/null org-groups 2>&1 || true) + echo "$output" | grep -q "No organization ID" + + - name: "org-reports: requires handle" + shell: bash + run: | + output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" uv run hackerone --env-file /dev/null org-reports 2>&1 || true) + echo "$output" | grep -q "No program handle" + + - name: "org-metrics: requires handle" + shell: bash + run: | + output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" uv run hackerone --env-file /dev/null org-metrics 2>&1 || true) + echo "$output" | grep -q "No program handle" + + - name: "org-scopes: requires handle" + shell: bash + run: | + output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" uv run hackerone --env-file /dev/null org-scopes 2>&1 || true) + echo "$output" | grep -q "No program handle" + + - name: "org-invite-hacker: requires args" + shell: bash + run: | + output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" uv run hackerone --env-file /dev/null org-invite-hacker 2>&1 || true) + echo "$output" | grep -q "Usage" + + - name: "org-swag: requires report ID" + shell: bash + run: | + output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" uv run hackerone --env-file /dev/null org-swag 2>&1 || true) + echo "$output" | grep -q "Usage" + + # --- Scope (no creds needed) --- + + - name: "scope: works without credentials" + shell: bash + run: | + HACKERONE_USERNAME="" HACKERONE_API_KEY="" uv run hackerone --env-file /dev/null scope 2>&1 | grep -q "Invalid arguments" + + # --- Verbose flag --- + + - name: "verbose: shows auth info" shell: bash run: | - HACKERONE_USERNAME="" HACKERONE_API_KEY="" uv run hackerone --env-file /dev/null help + output=$(HACKERONE_USERNAME="x" HACKERONE_API_KEY="x" uv run hackerone --env-file /dev/null -v help 2>&1 || true) + # help doesn't show auth (no creds needed), but should still work + echo "$output" | grep -q "Hacker Modules" From 50d265e705aa9c0b688b2dca97a34a96ece1252f Mon Sep 17 00:00:00 2001 From: thereisnotime <37583483+thereisnotime@users.noreply.github.com> Date: Thu, 26 Mar 2026 22:03:44 +0200 Subject: [PATCH 12/12] Fix CI: match command names as substrings in JSON keys --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5460ec8..60e9362 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,9 +77,9 @@ jobs: run: | uv run hackerone help --json | python3 -c " import json,sys - cmds = json.load(sys.stdin)['commands'] - for c in ['balance','reports','programs','profile','earnings','payouts']: - assert c in cmds, f'missing {c}' + keys = ' '.join(json.load(sys.stdin)['commands'].keys()) + for c in ['balance','reports','programs','profile','earnings','payouts','report','program','burp','csv','scope']: + assert c in keys, f'missing {c}' " - name: "help --json: lists all org commands" @@ -87,9 +87,9 @@ jobs: run: | uv run hackerone help --json | python3 -c " import json,sys - cmds = json.load(sys.stdin)['commands'] - for c in ['org','org-members ','org-reports []','org-bounty ','org-swag ']: - assert c in cmds, f'missing {c}' + keys = ' '.join(json.load(sys.stdin)['commands'].keys()) + for c in ['org','org-members','org-reports','org-report','org-update-report','org-activities','org-metrics','org-scopes','org-invite-hacker','org-bounty','org-swag']: + assert c in keys, f'missing {c}' " # --- Error handling ---