agents/openai.yaml
interface:
display_name: "Engagement Scope"
short_description: "Frame authorized cyber work."
default_prompt: "Use engagement-scope to confirm authorization, define boundaries, and route this cyber task to the right specialized skill."
references/bounty-scope-sources.md
# Bounty Scope Sources
Use this reference when a target appears to be covered by a public bug bounty, vulnerability disclosure, or coordinated vulnerability disclosure program. Always verify live program scope before testing; these URLs are lookup starting points, not cached authorization.
Store curated program metadata in the open target workspace's `data/` directory. Expect one bounty or disclosure program per workspace.
## Source Priority
1. User-provided private invitation, contract, rules of engagement, or current program brief.
2. Curated workspace-local program metadata in `data/`.
3. Workspace-local disposable fetch cache in `data/.cache/`.
4. Official platform program page or official platform API for the named program.
5. Official company security, bounty, or vulnerability disclosure page.
6. Official `security.txt` at `https://<domain>/.well-known/security.txt`.
7. Search results, public writeups, program directories, or third-party aggregators as leads only.
Never treat a brand-owned domain, subsidiary, ASN, repository, package, or mobile app as in scope unless the official program says so. If a scope page says only listed assets are in scope, do not infer neighboring assets.
## HackerOne
- Program page pattern: `https://hackerone.com/<handle>?type=team`
- Use `scripts/hackerone_program_lookup.py` for public GraphQL lookup when a handle is known.
- The lookup script caches public results for 6 hours by default under the active workspace's `data/.cache/hackerone/`. Use `--refresh` for current verification and include cache status in the engagement brief.
- HackerOne's authenticated Hacker API also has structured scope and scope exclusion endpoints, but it requires an API username and token.
- HackerOne scope fields to preserve: `eligible_for_submission`, `eligible_for_bounty`, asset identifier, asset type, max severity, asset instructions, scope exclusions, safe harbor, submission state, and policy change timestamp.
## Bugcrowd
- Search official engagements first: `site:bugcrowd.com/engagements <program or company>`.
- Individual public engagement pages commonly live under `https://bugcrowd.com/engagements/<slug>`.
- Read the engagement brief, especially Overview, Description, Targets, Known Issues, What's New, Things to Know, Testing Requirements, Disclosure, Safe Harbor, and target reward flags.
- Bugcrowd documentation says the brief's Targets section distinguishes in-scope and out-of-scope targets and that reports against targets not explicitly in scope may be marked out of scope.
## Intigriti
- Search official public program pages first: `site:app.intigriti.com/programs <program or company>`.
- Public program pages commonly use `https://app.intigriti.com/programs/<organization>/<program>`.
- Preserve assets, tiers, asset types, skills, rules, validation timelines, exclusions, disclosure terms, and any required account or VPN instructions.
## YesWeHack
- Search official public program pages first: `site:yeswehack.com/programs <program or company>`.
- Public program pages commonly use `https://yeswehack.com/programs/<slug>`.
- YesWeHack guidance treats scope as the assets hunters are invited to test and recommends treating anything not listed as out of scope by default unless the program states otherwise.
- Preserve scope, type, asset value, reward grid, user-agent, VPN, credentials, disclosure terms, and program version notes.
## Major Direct Programs
- Apple Security Bounty: `https://security.apple.com/bounty/`
- Google Bug Hunters: `https://bughunters.google.com/`
- Google and Alphabet VRP rules: `https://bughunters.google.com/about/rules/google-friends/google-and-alphabet-vulnerability-reward-program-vrp-rules`
- GitHub Bug Bounty scope: `https://bounty.github.com/scope`
- Microsoft Bounty Programs: `https://www.microsoft.com/en-us/msrc/bounty`
- Meta Bug Bounty: `https://www.facebook.com/whitehat`
- Mozilla Security Bug Bounty: `https://www.mozilla.org/en-US/security/bug-bounty/`
When these pages link to product-specific subprograms, rules, target lists, payout guidelines, legal safe harbor, or reporting requirements, follow those official links and use the most specific page for the target.
## Evidence To Capture
For the engagement brief, record:
- Source URL and retrieval date.
- Program name, platform, and handle or slug.
- In-scope targets and asset types.
- Explicitly out-of-scope targets and vulnerability classes.
- Submission and bounty eligibility.
- Safe harbor or legal terms.
- Testing limits, rate limits, account rules, VPN or user-agent requirements, and prohibited techniques.
- Whether production testing is allowed or a lab/staging/local reproduction is required.
If scope is ambiguous, ask the user to provide the current brief or request clarification from the program before continuing.
references/program-scope.v1.schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://program-scope.local/schemas/program-scope.v1.schema.json",
"title": "Program Scope Record v1",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"program_id",
"name",
"owner",
"platform",
"program_type",
"scope_model",
"official_urls",
"source_records",
"safe_harbor",
"testing_limits",
"last_verified",
"cache_policy",
"notes"
],
"properties": {
"schema_version": {
"const": "program-scope.v1"
},
"program_id": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9./_-]*[a-z0-9]$"
},
"parent_program_id": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9./_-]*[a-z0-9]$"
},
"name": {
"type": "string",
"minLength": 1
},
"owner": {
"type": "string",
"minLength": 1
},
"platform": {
"type": "string",
"enum": [
"direct",
"hackerone",
"bugcrowd",
"intigriti",
"yeswehack",
"other"
]
},
"platform_handle": {
"type": "string",
"minLength": 1
},
"program_type": {
"type": "string",
"enum": [
"direct",
"platform"
]
},
"scope_model": {
"type": "string",
"enum": [
"listed-assets",
"product-family",
"open-with-exclusions",
"mixed",
"unknown"
]
},
"official_urls": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"format": "uri"
}
},
"source_records": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9./_-]*\\.ya?ml$"
}
},
"subrecords": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"type",
"path",
"description"
],
"properties": {
"id": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9_-]*[a-z0-9]$"
},
"type": {
"type": "string",
"enum": [
"product",
"product-group",
"subprogram",
"subprogram-group"
]
},
"path": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9./_-]*$"
},
"description": {
"type": "string",
"minLength": 1
}
}
}
},
"safe_harbor": {
"type": "object",
"additionalProperties": false,
"required": [
"status",
"urls",
"notes"
],
"properties": {
"status": {
"type": "string",
"enum": [
"documented",
"linked",
"unknown",
"none"
]
},
"urls": {
"type": "array",
"items": {
"type": "string",
"format": "uri"
}
},
"notes": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"testing_limits": {
"type": "array",
"items": {
"type": "string"
}
},
"last_verified": {
"type": "string",
"format": "date"
},
"cache_policy": {
"type": "object",
"additionalProperties": false,
"required": [
"ttl_seconds",
"refresh_required_before_testing"
],
"properties": {
"ttl_seconds": {
"type": "integer",
"minimum": 0
},
"refresh_required_before_testing": {
"type": "boolean"
}
}
},
"notes": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
scripts/goal_workspace.py
#!/usr/bin/env python3
"""Create a per-goal workspace tree for Codex ATTACK goal runs."""
from __future__ import annotations
import argparse
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
GOAL_SUBDIRS = ("recon", "modeling", "proofing", "reports")
SLUG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
def now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
def validate_goal_id(value: str) -> str:
goal_id = value.strip().lower()
if not SLUG_RE.fullmatch(goal_id):
raise argparse.ArgumentTypeError(
"goal ID must be a lower-case slug like pragma-elevation"
)
return goal_id
def bullet(items: list[str]) -> str:
if not items:
return "- none"
return "\n".join(f"- {item}" for item in items)
def brief_text(args: argparse.Namespace, root: Path) -> str:
return f"""# Goal: {args.goal_id}
Created: {now()}
Artifact root: {root.as_posix()}
## Objective
{args.objective}
## Target Boundary
{args.target}
## Completion Condition
{args.completion}
## Non-Goals
{bullet(args.non_goal or [])}
## Notes
{args.notes or "none"}
"""
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(
description="Create goal/<goal-id>/{recon,modeling,proofing,reports}."
)
parser.add_argument("--goal-id", required=True, type=validate_goal_id)
parser.add_argument("--objective", required=True)
parser.add_argument("--target", required=True)
parser.add_argument("--completion", required=True)
parser.add_argument("--non-goal", action="append")
parser.add_argument("--notes", default="")
parser.add_argument("--workspace", default=".")
parser.add_argument("--force", action="store_true", help="overwrite brief.md")
args = parser.parse_args(argv)
workspace = Path(args.workspace).expanduser()
root = workspace / "goal" / args.goal_id
root.mkdir(parents=True, exist_ok=True)
for name in GOAL_SUBDIRS:
(root / name).mkdir(exist_ok=True)
brief_path = root / "brief.md"
if brief_path.exists() and not args.force:
print(f"error: refusing to overwrite {brief_path}; pass --force", file=sys.stderr)
return 2
brief_path.write_text(brief_text(args, root), encoding="utf-8")
print(root.as_posix())
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
scripts/hackerone_program_lookup.py
#!/usr/bin/env python3
"""Look up a public HackerOne program through HackerOne's public GraphQL endpoint."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import sys
import textwrap
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any
GRAPHQL_URL = "https://hackerone.com/graphql"
CACHE_SCHEMA_VERSION = 1
DEFAULT_CACHE_TTL_SECONDS = 6 * 60 * 60
QUERY = """
query PublicProgramScope(
$handle: String!
$from: Int
$size: Int
$searchString: String
$eligibleForSubmission: Boolean
$eligibleForBounty: Boolean
) {
team(handle: $handle) {
id
handle
name
state
url
offers_bounties
offers_thanks
submission_state
external_url
scope_description
policy_setting {
id
policy
last_policy_change_at
}
external_program {
id
policy_url
policy
disclosure_url
offers_rewards
thanks_url
}
declarative_policy {
id
has_open_scope
protected_by_gold_standard_safe_harbor
protected_by_ai_safe_harbor
scope_exclusions {
id
category
details
}
}
structured_scopes_search(
from: $from
size: $size
search_string: $searchString
eligible_for_submission: $eligibleForSubmission
eligible_for_bounty: $eligibleForBounty
) {
total_count
nodes {
... on StructuredScopeDocument {
id
identifier
display_name
instruction
eligible_for_bounty
eligible_for_submission
cvss_score
asset_type
created_at
updated_at
}
}
}
}
}
""".strip()
def normalize_handle(value: str) -> str:
raw = value.strip()
if not raw:
raise ValueError("program handle is empty")
if "://" in raw:
parsed = urllib.parse.urlparse(raw)
host = parsed.netloc.lower()
if not (host == "hackerone.com" or host.endswith(".hackerone.com")):
raise ValueError(f"not a HackerOne URL: {value}")
parts = [part for part in parsed.path.split("/") if part]
if not parts:
raise ValueError(f"could not find a program handle in URL: {value}")
raw = parts[0]
raw = raw.strip().strip("/")
if not re.fullmatch(r"[A-Za-z0-9_-]{1,80}", raw):
raise ValueError(
"HackerOne handles should contain only letters, digits, underscores, or hyphens"
)
return raw
def default_cache_dir() -> Path:
override = os.environ.get("SCOPE_CACHE_DIR")
if override:
return Path(override).expanduser()
return Path.cwd() / "data" / ".cache" / "hackerone"
def parse_cache_ttl(value: str | None) -> int:
if value is None or value == "":
return DEFAULT_CACHE_TTL_SECONDS
try:
ttl = int(value)
except ValueError as exc:
raise ValueError("cache TTL must be an integer number of seconds") from exc
if ttl < 0:
raise ValueError("cache TTL must be zero or greater")
return ttl
def iso_utc(timestamp: float) -> str:
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(timestamp))
def make_cache_key(params: dict[str, Any]) -> str:
encoded = json.dumps(params, sort_keys=True, separators=(",", ":")).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()[:24]
def cache_path_for(cache_dir: Path, params: dict[str, Any]) -> Path:
handle = params["handle"]
return cache_dir / f"{handle}-{make_cache_key(params)}.json"
def load_cached_program(
cache_path: Path, cache_ttl: int
) -> tuple[dict[str, Any], dict[str, Any]] | None:
if cache_ttl <= 0:
return None
try:
with cache_path.open("r", encoding="utf-8") as handle:
record = json.load(handle)
except (FileNotFoundError, json.JSONDecodeError, OSError):
return None
if record.get("schema_version") != CACHE_SCHEMA_VERSION:
return None
fetched_at = float(record.get("fetched_at") or 0)
age_seconds = max(0, int(time.time() - fetched_at))
if age_seconds > cache_ttl:
return None
program = record.get("program")
if not isinstance(program, dict):
return None
return program, {
"status": "hit",
"path": str(cache_path),
"fetched_at": iso_utc(fetched_at),
"age_seconds": age_seconds,
"ttl_seconds": cache_ttl,
}
def write_cached_program(cache_path: Path, program: dict[str, Any]) -> dict[str, Any]:
fetched_at = time.time()
cache_path.parent.mkdir(parents=True, exist_ok=True)
tmp_path = cache_path.with_suffix(cache_path.suffix + ".tmp")
record = {
"schema_version": CACHE_SCHEMA_VERSION,
"fetched_at": fetched_at,
"program": program,
}
with tmp_path.open("w", encoding="utf-8") as handle:
json.dump(record, handle, indent=2, sort_keys=True)
handle.write("\n")
os.replace(tmp_path, cache_path)
return {
"status": "refresh",
"path": str(cache_path),
"fetched_at": iso_utc(fetched_at),
"age_seconds": 0,
"ttl_seconds": None,
}
def annotate_cache(program: dict[str, Any], cache_status: dict[str, Any] | None) -> dict[str, Any]:
if not cache_status:
return program
annotated = dict(program)
source = dict(annotated.get("source") or {})
source["cache"] = cache_status
annotated["source"] = source
return annotated
def graphql_request(
handle: str,
offset: int,
size: int,
timeout: int,
search: str | None,
eligible_for_submission: bool | None,
eligible_for_bounty: bool | None,
) -> dict[str, Any]:
payload = {
"operationName": "PublicProgramScope",
"variables": {
"handle": handle,
"from": offset,
"size": size,
"searchString": search,
"eligibleForSubmission": eligible_for_submission,
"eligibleForBounty": eligible_for_bounty,
},
"query": QUERY,
}
request = urllib.request.Request(
GRAPHQL_URL,
data=json.dumps(payload).encode("utf-8"),
headers={
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "scope-lookup/0.1",
"X-Requested-With": "XMLHttpRequest",
},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
return json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(f"HackerOne returned HTTP {exc.code}: {body[:500]}") from exc
except urllib.error.URLError as exc:
raise RuntimeError(f"could not reach HackerOne: {exc}") from exc
def fetch_program_live(
handle: str,
*,
limit: int,
page_size: int,
fetch_all: bool,
timeout: int,
search: str | None,
eligible_for_submission: bool | None,
eligible_for_bounty: bool | None,
) -> dict[str, Any]:
first_size = page_size if fetch_all else limit
data = graphql_request(
handle,
0,
first_size,
timeout,
search,
eligible_for_submission,
eligible_for_bounty,
)
if data.get("errors"):
messages = "; ".join(error.get("message", str(error)) for error in data["errors"])
raise RuntimeError(f"HackerOne GraphQL error: {messages}")
team = (data.get("data") or {}).get("team")
if team is None:
raise RuntimeError(f"no public HackerOne program found for handle: {handle}")
scope_search = team.get("structured_scopes_search") or {}
nodes = list(scope_search.get("nodes") or [])
total = int(scope_search.get("total_count") or len(nodes))
if fetch_all and len(nodes) < total:
offset = len(nodes)
while offset < total:
page = graphql_request(
handle,
offset,
page_size,
timeout,
search,
eligible_for_submission,
eligible_for_bounty,
)
if page.get("errors"):
messages = "; ".join(
error.get("message", str(error)) for error in page["errors"]
)
raise RuntimeError(f"HackerOne GraphQL error: {messages}")
page_team = (page.get("data") or {}).get("team") or {}
page_search = page_team.get("structured_scopes_search") or {}
page_nodes = list(page_search.get("nodes") or [])
if not page_nodes:
break
nodes.extend(page_nodes)
offset += len(page_nodes)
team["structured_scopes"] = nodes
team["structured_scope_total_count"] = total
team.pop("structured_scopes_search", None)
team["source"] = {
"graphql_url": GRAPHQL_URL,
"program_url": f"https://hackerone.com/{handle}?type=team",
"filters": {
"search": search,
"eligible_for_submission": eligible_for_submission,
"eligible_for_bounty": eligible_for_bounty,
},
"note": "Best-effort public lookup. Verify current scope on the official program page before testing.",
}
return team
def lookup_program(
handle: str,
*,
limit: int,
page_size: int,
fetch_all: bool,
timeout: int,
search: str | None,
eligible_for_submission: bool | None,
eligible_for_bounty: bool | None,
cache_dir: Path,
cache_ttl: int,
refresh: bool,
no_cache: bool,
) -> dict[str, Any]:
cache_params = {
"schema_version": CACHE_SCHEMA_VERSION,
"source": "hackerone-graphql",
"handle": handle,
"limit": None if fetch_all else limit,
"fetch_all": fetch_all,
"search": search,
"eligible_for_submission": eligible_for_submission,
"eligible_for_bounty": eligible_for_bounty,
}
cache_path = cache_path_for(cache_dir, cache_params)
if not no_cache and not refresh:
cached = load_cached_program(cache_path, cache_ttl)
if cached is not None:
program, cache_status = cached
return annotate_cache(program, cache_status)
program = fetch_program_live(
handle,
limit=limit,
page_size=page_size,
fetch_all=fetch_all,
timeout=timeout,
search=search,
eligible_for_submission=eligible_for_submission,
eligible_for_bounty=eligible_for_bounty,
)
if no_cache:
cache_status = {"status": "disabled"}
else:
cache_status = write_cached_program(cache_path, program)
cache_status["ttl_seconds"] = cache_ttl
return annotate_cache(program, cache_status)
def parse_optional_bool(value: str) -> bool | None:
lowered = value.lower()
if lowered in {"any", "unknown", "null"}:
return None
if lowered in {"yes", "true", "1"}:
return True
if lowered in {"no", "false", "0"}:
return False
raise argparse.ArgumentTypeError("expected one of: yes, no, any")
def bool_text(value: Any) -> str:
if value is True:
return "yes"
if value is False:
return "no"
return "unknown"
def truncate(value: str | None, max_chars: int) -> str:
if not value:
return ""
collapsed = re.sub(r"\s+", " ", value).strip()
if len(collapsed) <= max_chars:
return collapsed
return collapsed[: max_chars - 3].rstrip() + "..."
def markdown_escape(value: Any) -> str:
text = "" if value is None else str(value)
return text.replace("|", "\\|").replace("\n", " ")
def render_markdown(program: dict[str, Any], policy_chars: int) -> str:
policy = (program.get("policy_setting") or {}).get("policy")
external_policy = (program.get("external_program") or {}).get("policy")
declarative = program.get("declarative_policy") or {}
scopes = program.get("structured_scopes") or []
lines = [
f"# HackerOne Program Scope: {program.get('name') or program.get('handle')}",
"",
f"- Handle: `{program.get('handle')}`",
f"- Source: {program.get('source', {}).get('program_url')}",
f"- State: `{program.get('state')}`",
f"- Submission state: `{program.get('submission_state')}`",
f"- Offers bounties: {bool_text(program.get('offers_bounties'))}",
f"- Offers thanks: {bool_text(program.get('offers_thanks'))}",
f"- Open scope: {bool_text(declarative.get('has_open_scope'))}",
"- Gold safe harbor: "
f"{bool_text(declarative.get('protected_by_gold_standard_safe_harbor'))}",
"- AI safe harbor: "
f"{bool_text(declarative.get('protected_by_ai_safe_harbor'))}",
f"- Structured scopes shown: {len(scopes)} of "
f"{program.get('structured_scope_total_count', len(scopes))}",
]
cache = (program.get("source") or {}).get("cache")
if cache:
lines.append(
"- Cache: "
f"{cache.get('status')}"
+ (f" from `{cache.get('path')}`" if cache.get("path") else "")
+ (f", fetched {cache.get('fetched_at')}" if cache.get("fetched_at") else "")
)
exclusions = declarative.get("scope_exclusions") or []
if exclusions:
lines.extend(["", "## Scope Exclusions", ""])
for item in exclusions:
category = item.get("category") or "uncategorized"
details = truncate(item.get("details"), 240)
lines.append(f"- `{category}`: {details}")
if scopes:
lines.extend(
[
"",
"## Structured Scopes",
"",
"| Submission | Bounty | Type | Identifier | Severity | Notes |",
"| --- | --- | --- | --- | --- | --- |",
]
)
for scope in scopes:
identifier = scope.get("identifier") or scope.get("display_name")
notes = truncate(scope.get("instruction"), 160)
lines.append(
"| "
f"{bool_text(scope.get('eligible_for_submission'))} | "
f"{bool_text(scope.get('eligible_for_bounty'))} | "
f"{markdown_escape(scope.get('asset_type'))} | "
f"`{markdown_escape(identifier)}` | "
f"{markdown_escape(scope.get('cvss_score'))} | "
f"{markdown_escape(notes)} |"
)
if policy_chars > 0 and (policy or external_policy):
lines.extend(["", "## Policy Excerpt", ""])
lines.append(truncate(policy or external_policy, policy_chars))
return "\n".join(lines) + "\n"
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Look up public HackerOne program scope via HackerOne GraphQL."
)
parser.add_argument("program", help="HackerOne handle or https://hackerone.com/<handle> URL")
parser.add_argument(
"--format",
choices=("json", "markdown"),
default="json",
help="output format",
)
parser.add_argument(
"--limit",
type=int,
default=100,
help="number of structured scopes to fetch unless --all is set",
)
parser.add_argument("--all", action="store_true", help="fetch all structured scopes")
parser.add_argument("--page-size", type=int, default=100, help="page size for --all")
parser.add_argument("--timeout", type=int, default=20, help="request timeout in seconds")
parser.add_argument(
"--cache-dir",
default=str(default_cache_dir()),
help="directory for persistent cache files; defaults to workspace data/.cache/hackerone",
)
parser.add_argument(
"--cache-ttl",
type=int,
default=parse_cache_ttl(os.environ.get("SCOPE_CACHE_TTL")),
help="cache freshness window in seconds; 0 disables cache reads",
)
parser.add_argument("--refresh", action="store_true", help="ignore cache and fetch fresh data")
parser.add_argument(
"--no-cache",
action="store_true",
help="disable cache reads and writes for this lookup",
)
parser.add_argument("--search", help="filter structured scopes by search text")
parser.add_argument(
"--eligible-for-submission",
type=parse_optional_bool,
default=None,
metavar="{yes,no,any}",
help="filter assets by submission eligibility",
)
parser.add_argument(
"--eligible-for-bounty",
type=parse_optional_bool,
default=None,
metavar="{yes,no,any}",
help="filter assets by bounty eligibility",
)
parser.add_argument(
"--policy-chars",
type=int,
default=2000,
help="policy excerpt length for markdown output",
)
args = parser.parse_args(argv)
try:
handle = normalize_handle(args.program)
if args.limit < 1:
raise ValueError("--limit must be at least 1")
if args.page_size < 1:
raise ValueError("--page-size must be at least 1")
if args.cache_ttl < 0:
raise ValueError("--cache-ttl must be zero or greater")
program = lookup_program(
handle,
limit=args.limit,
page_size=args.page_size,
fetch_all=args.all,
timeout=args.timeout,
search=args.search,
eligible_for_submission=args.eligible_for_submission,
eligible_for_bounty=args.eligible_for_bounty,
cache_dir=Path(args.cache_dir).expanduser(),
cache_ttl=args.cache_ttl,
refresh=args.refresh,
no_cache=args.no_cache,
)
if args.format == "markdown":
sys.stdout.write(render_markdown(program, args.policy_chars))
else:
json.dump(program, sys.stdout, indent=2, sort_keys=True)
sys.stdout.write("\n")
except Exception as exc:
wrapped = textwrap.fill(str(exc), width=88)
sys.stderr.write(f"error: {wrapped}\n")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
scripts/validate_program_records.py
#!/usr/bin/env python3
"""Validate program records in the open workspace data directory."""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
import yaml
PLUGIN_SKILL_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_SCHEMA_PATH = PLUGIN_SKILL_ROOT / "references" / "program-scope.v1.schema.json"
class ValidationError(Exception):
pass
def type_matches(value: Any, expected: str) -> bool:
if expected == "object":
return isinstance(value, dict)
if expected == "array":
return isinstance(value, list)
if expected == "string":
return isinstance(value, str)
if expected == "integer":
return isinstance(value, int) and not isinstance(value, bool)
if expected == "boolean":
return isinstance(value, bool)
return True
def check_format(value: str, fmt: str, path: str) -> None:
if fmt == "uri":
parsed = urlparse(value)
if not parsed.scheme or not parsed.netloc:
raise ValidationError(f"{path}: expected absolute URI")
elif fmt == "date":
if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", value):
raise ValidationError(f"{path}: expected YYYY-MM-DD date")
def validate_value(value: Any, schema: dict[str, Any], path: str) -> None:
if "const" in schema and value != schema["const"]:
raise ValidationError(f"{path}: expected {schema['const']!r}")
if "enum" in schema and value not in schema["enum"]:
raise ValidationError(f"{path}: expected one of {schema['enum']!r}")
expected_type = schema.get("type")
if expected_type and not type_matches(value, expected_type):
raise ValidationError(f"{path}: expected {expected_type}")
if isinstance(value, str):
if "minLength" in schema and len(value) < schema["minLength"]:
raise ValidationError(f"{path}: shorter than minLength")
if "pattern" in schema and not re.fullmatch(schema["pattern"], value):
raise ValidationError(f"{path}: does not match pattern {schema['pattern']!r}")
if "format" in schema:
check_format(value, schema["format"], path)
if isinstance(value, int) and not isinstance(value, bool):
if "minimum" in schema and value < schema["minimum"]:
raise ValidationError(f"{path}: below minimum {schema['minimum']}")
if isinstance(value, list):
if "minItems" in schema and len(value) < schema["minItems"]:
raise ValidationError(f"{path}: fewer than minItems")
item_schema = schema.get("items")
if item_schema:
for index, item in enumerate(value):
validate_value(item, item_schema, f"{path}[{index}]")
if isinstance(value, dict):
required = schema.get("required", [])
for key in required:
if key not in value:
raise ValidationError(f"{path}: missing required key {key!r}")
properties = schema.get("properties", {})
if schema.get("additionalProperties") is False:
extras = sorted(set(value) - set(properties))
if extras:
raise ValidationError(f"{path}: unexpected keys {extras!r}")
for key, child in value.items():
if key in properties:
validate_value(child, properties[key], f"{path}.{key}")
def load_yaml(path: Path) -> dict[str, Any]:
with path.open("r", encoding="utf-8") as handle:
data = yaml.safe_load(handle)
if not isinstance(data, dict):
raise ValidationError(f"{path}: expected YAML mapping")
return data
def program_records(data_root: Path) -> list[Path]:
program_root = data_root
records = [
path
for path in program_root.rglob("*.yaml")
if path.name != "sources.yaml" and ".cache" not in path.parts
]
return sorted(records)
def main() -> int:
import argparse
parser = argparse.ArgumentParser(
description="Validate program YAML records in a workspace data directory."
)
parser.add_argument(
"--data-dir",
default=str(Path.cwd() / "data"),
help="workspace data directory; defaults to ./data",
)
parser.add_argument(
"--schema",
default=str(DEFAULT_SCHEMA_PATH),
help="program-scope v1 JSON schema path",
)
args = parser.parse_args()
workspace_root = Path.cwd()
data_root = Path(args.data_dir).expanduser().resolve()
schema_path = Path(args.schema).expanduser().resolve()
schema = json.loads(schema_path.read_text(encoding="utf-8"))
records = program_records(data_root)
if not records:
print(f"No program records found under {data_root}.", file=sys.stderr)
return 1
failures: list[str] = []
for record_path in records:
try:
try:
display_path = str(record_path.relative_to(workspace_root))
except ValueError:
display_path = str(record_path)
validate_value(load_yaml(record_path), schema, display_path)
except Exception as exc:
failures.append(f"{display_path}: {exc}")
if failures:
print("Program record validation failed:", file=sys.stderr)
for failure in failures:
print(f"- {failure}", file=sys.stderr)
return 1
try:
display_schema = schema_path.relative_to(workspace_root)
except ValueError:
display_schema = schema_path
print(f"Validated {len(records)} program record(s) against {display_schema}.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
SKILL.md
---
name: engagement-scope
description: Establish authorized engagement context, look up public bounty scope evidence, and route cyber tasks before using specialized skills. Use for any request to model security boundaries, review code vulnerabilities, inspect web or mobile apps, research CVEs, build fuzzers, reverse or debug binaries, or analyze exploit chains, especially when target authorization, bounty program scope, impact tolerance, or the desired deliverable is unclear.
---
# Engagement Scope
Use this skill as the first stop for scoped cyber workflows. Define what is authorized, what is in scope, what level of impact is acceptable, and which specialized skill should carry the work forward.
## Scope Workflow
1. Identify the requested security activity.
2. Determine whether the target, authorization context, and boundaries are already clear from the conversation, local workspace, or provided materials.
3. If the user names a public bounty or vulnerability disclosure program, collect current scope evidence from official sources before assuming coverage.
4. Ask only for missing information that changes whether work can proceed safely or changes the technical approach.
5. Produce a short engagement brief before handing off to a specialized skill.
6. Re-check scope before any step that would materially increase impact, touch live external systems, attempt exploitation, collect sensitive data, or alter target state.
## Authorization Check
Proceed when there is a clear authorized context, such as:
- The target is source code, binaries, logs, configs, or infrastructure artifacts supplied by the user in the workspace.
- The user states they own or administer the target.
- The user provides a bug bounty, pentest, audit, lab, CTF, or internal security-testing context with scope boundaries.
- The task is non-destructive analysis, reproduction in a local lab, hardening, or report preparation for an authorized finding.
Pause and ask a concise scope question when:
- The target is a live third-party system and authorization is not stated.
- The request could affect availability, integrity, confidentiality, accounts, data, or logs outside a local lab.
## Public Scope Lookup
When a user mentions a public bounty platform, program handle, or official VRP, verify scope from official sources before treating the target as authorized. Prefer current, specific program rules over general company ownership.
Source priority:
1. User-provided private invitation, contract, rules of engagement, or current program brief.
2. Curated program metadata in the open workspace's `data/` directory, validated by this skill's `references/program-scope.v1.schema.json`.
3. Disposable fetch cache in the open workspace's `data/.cache/`, only as a freshness/performance aid.
4. Official platform program page or official platform API for the named program.
5. Official company security, bounty, or vulnerability disclosure page.
6. Official `security.txt` as a lead when no program page is known.
7. Third-party directories, public writeups, or search snippets as discovery leads only.
Program data belongs to the active target workspace. Expect one bounty or disclosure program per workspace. A scalable workspace layout is:
```text
data/
program.yaml
sources.yaml
products/
subprograms/
scope/
.cache/
```
Use `program.yaml` for the workspace's top-level program record. Use `products/` or `subprograms/` for large direct programs such as Apple Security Bounty or MSRC when product-specific rules diverge. Keep raw fetched pages, API responses, and query results in `data/.cache/` and do not treat cache files as curated scope authority.
For HackerOne public programs, use `scripts/hackerone_program_lookup.py` when a handle is known. Resolve the script path relative to this skill directory and run it from the active target workspace:
```bash
python <skill-dir>/scripts/hackerone_program_lookup.py <handle> --format markdown --eligible-for-submission yes --limit 50
python <skill-dir>/scripts/hackerone_program_lookup.py <handle> --format markdown --search <asset-or-domain> --limit 20
python <skill-dir>/scripts/hackerone_program_lookup.py <handle> --format json --all
python <skill-dir>/scripts/hackerone_program_lookup.py <handle> --refresh --format markdown --eligible-for-submission yes
```
Use `--eligible-for-submission yes` to focus on testable assets, `--eligible-for-bounty yes` to focus on bounty-eligible assets, and `--search` for a specific domain, repository, app, or package. Treat the script as a best-effort public lookup through HackerOne's current GraphQL surface; verify important decisions against the official program page at `https://hackerone.com/<handle>?type=team`.
The HackerOne script uses a persistent workspace-local cache by default to avoid repeated scope pulls across conversations. Default cache location is `data/.cache/hackerone/` under the current working directory. Default TTL is 6 hours. Use `--refresh` before active testing, when the user asks for current scope, or when cache metadata is older than the engagement requires. Use `--no-cache` for one-off uncached reads, `--cache-dir` to isolate a cache, or `SCOPE_CACHE_DIR` / `SCOPE_CACHE_TTL` to change defaults.
Validate curated workspace records with:
```bash
python <skill-dir>/scripts/validate_program_records.py --data-dir data
```
For Bugcrowd, Intigriti, YesWeHack, and major direct programs, read `references/bounty-scope-sources.md` only when needed. Use it to find official scope pages and to decide what evidence to capture.
## Engagement Brief
Before routing, summarize the working frame in compact form:
```text
Authorized context: <what establishes permission or what remains unknown>
Scope evidence: <official program URL, API/script result, cache hit/refresh, date checked, policy version/change date>
Target assets: <repos, apps, hosts, binaries, versions, files, accounts>
In scope: <allowed systems, code paths, vulnerability classes, techniques>
Out of scope: <systems, data, techniques, timing, impact limits>
Objective: <review, reproduce, fuzz, reverse, debug, research, report>
Impact tolerance: <read-only, local-only, low-rate live testing, invasive lab testing>
Deliverable: <findings, patch, PoC-in-lab, human PoC kit, harness, threat model, report>
Next skill: <specialized skill>
```
Use `unknown` sparingly. If an unknown affects safety or execution, ask before proceeding.
When scope comes from a public bounty program, include exact asset identifiers or rule text summaries. Do not collapse a precise target list into a broad phrase like "company assets" unless the official program explicitly defines an open scope.
## Goal Support
Use Codex goals for long-running, multi-turn security work that has a concrete
finish line and benefits from autonomous continuation across context compaction,
subagents, or several work phases. Keep goal use general: do not maintain narrow
campaign-specific templates. Scope each goal from the engagement brief and the
user's current objective.
Use a goal when the work is too large for one normal turn but still bounded, for
example: review one subsystem, validate one vulnerability class across a defined
target, investigate one crash family, prove or de-escalate one exploit-chain
hypothesis, or produce one report package. Do not start a goal for quick
questions, one-off edits, simple lookups, or work whose target, deliverable, or
stopping condition is still vague.
Codex supports only one active goal per session. Before planning or starting a
new goal, check the current goal state with `get_goal` when that tool is
available. If a goal is already active, do not create a competing plan or start a
second goal. Instead, continue the active goal, explicitly connect the user's
new request to it, or explain that the current goal needs to be completed or
blocked before a new goal can begin.
Good goal scope has:
- One target boundary: repository, app, binary, package, component, workflow, or
finding ID.
- One primary objective: discover, validate, reproduce, proof, fix, or report.
- A clear completion condition that can be marked done without open-ended
exploration.
- Explicit non-goals for adjacent systems, vulnerability classes, exploit-chain
branches, or report polish that would make the run sprawl.
- A practical time, turn, token, or evidence budget when the user supplied one
or the work could otherwise run too long.
- A handoff path through `finding-tracker`, `triage-verifier`, or
`report-writer` when those state transitions are in play.
Use one top-level `goal/` directory in the active target workspace. Each goal
gets a unique lower-case slug ID and its own subdirectory:
```text
goal/
<goal-id>/
recon/
modeling/
proofing/
reports/
```
Examples: `goal/pragma-elevation/recon/`, `goal/stripe-context-loss/modeling/`,
or `goal/avatar-import-ssrf/proofing/`. Do not create shared directories such
as `goal/recon/` or multiple top-level goal roots. Put the goal brief,
checkpoints, notes, and generated artifacts under the current
`goal/<goal-id>/` tree.
Use the helper script to create the directory tree after the active-goal check
passes and the user has agreed to the bounded goal:
```bash
python <skill-dir>/scripts/goal_workspace.py \
--goal-id pragma-elevation \
--objective "Validate whether the sudo helper permits local elevation." \
--target "repo tools/sudo-helper and its tests" \
--completion "Finding is proofed, de-escalated, or fixed with regression tests." \
--non-goal "Review unrelated privilege boundaries"
```
When proposing a goal, include:
```text
Goal ID: <slug>
Objective: <single bounded outcome>
Target boundary: <exact target>
Completion condition: <what makes the goal complete>
Non-goals: <what is intentionally excluded>
Artifact root: goal/<goal-id>/
Active-goal check: <none active / existing goal to continue>
```
## Routing
Route to one primary skill. Mention secondary skills only when they are likely to be needed later.
- Use `code-vulnerability-review` for source-code audits, insecure patterns, dependency risk, data-flow review, patch review, and remediation planning.
- Use `web-app-security-inspection` for browser-visible behavior, HTTP flows, authentication, authorization, API testing, session handling, and live or local web apps.
- Use `cve-research` for vulnerability intelligence, affected-version checks, exploitability analysis from public sources, and advisory triage.
- Use `fuzz-harness-builder` for parser, protocol, API, CLI, library, or file-format fuzzing and deterministic test harnesses.
- Use `binary-debugging` for runtime inspection, crash triage, debugger workflows, memory state, symbols, and dynamic behavior.
- Use `binary-reversing` for static analysis, disassembly, decompilation, binary structure, protocols, and undocumented behavior.
- Use `subagent-orchestration` when the user explicitly asks for subagents,
delegation, parallel agents, debate, or staged multi-agent security work.
Route there only after the engagement brief is clear enough to split the work
into bounded independent assignments.
- Use `exploit-chain-analysis` for authorized lab-only proof of exploitability, chaining multiple validated findings, risk demonstration, and report evidence.
- Use `auto-triage` for proofed findings that need human-operated, screen-recordable PoC packages or submission attachments.
## Question Style
Ask the smallest useful question set. Prefer one question when possible.
Useful questions include:
- "What target assets are in scope for this engagement?"
- "What authorization context applies to this target?"
- "Should this stay read-only/local, or is controlled live testing allowed?"
- "What deliverable do you want: findings, a patch, a lab reproduction, a harness, or a report?"
- "Is there a bounty program handle, official scope URL, or private invitation I should use as the authority?"
If enough information is already available, do not ask a ritual preflight. State the inferred scope and proceed.
## Handoff
When another specialized skill is used after this one, pass along the engagement brief and the immediate task. Keep the brief visible enough that later work does not drift from authorization, boundaries, and deliverable expectations.