chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
# Security alert triage
|
||||
|
||||
How Dependabot and CodeQL (code-scanning) alerts are managed for this repo.
|
||||
|
||||
## Pipeline
|
||||
|
||||
| Layer | Mechanism | What it does |
|
||||
|---|---|---|
|
||||
| Detection — deps | Dependabot alerts (on) | Flags vulnerable dependencies. |
|
||||
| Detection — code | CodeQL default setup (on) | Flags code-level findings. |
|
||||
| Detection — secrets | Secret scanning + push protection (on) | Blocks committed secrets. |
|
||||
| Detection — diff | `security-scan.yml` | Per-PR static scan (secrets/exfil/sensitive-path/workflow-misuse/semgrep/OSV). |
|
||||
| **Fixing — deps** | **Dependabot security updates** + `dependabot.yml` | Auto-opens grouped fix PRs for vulnerable deps. |
|
||||
| **Triage** | **`security-triage.yml`** (this) | Daily AI triage: dismiss high-confidence false positives, escalate serious findings privately. |
|
||||
|
||||
Dependency *fixing* is Dependabot's job; this workflow does not edit code. Code
|
||||
findings are never auto-fixed — only triaged.
|
||||
|
||||
## How the triage cron decides
|
||||
|
||||
The cron (`.github/workflows/security-triage.yml`) follows the same
|
||||
injection-resistant model as `issue-triage.yml`: trusted steps fetch alerts and
|
||||
apply mutations; the LLM (`.github/triage/security/`) runs with **no tools, no
|
||||
shell, no token** and only emits validated JSON.
|
||||
|
||||
Per alert the model returns one of:
|
||||
|
||||
- **false_positive** — pattern not exploitable here (must name why).
|
||||
- **wont_fix** — real but negligible (test-only fixture / dev-only tooling).
|
||||
- **serious** — real and exploitable in production / on untrusted input.
|
||||
- **monitor** — uncertain; left for a human.
|
||||
|
||||
Mutations are tightly gated:
|
||||
|
||||
- **Auto-dismiss** happens only at **confidence ≥ 0.9**, and is allow-listed
|
||||
on each side:
|
||||
- **CodeQL** — only for an allow-listed set of rule ids (see
|
||||
`AUTO_DISMISS_RULES` in the workflow). `py/path-injection` and
|
||||
`actions/untrusted-checkout` are **not** auto-dismissable.
|
||||
- **Dependabot** — only **low/medium** severity advisories. A **high or
|
||||
critical** dependency advisory is never auto-dismissed on the model's word
|
||||
alone; it always waits for a human.
|
||||
- **serious** findings are collected into a **private** GitHub Security
|
||||
Advisory draft. They are never posted to public issues.
|
||||
- **Mutations are OFF by default.** APPLY mode requires either the repo
|
||||
variable `SECURITY_TRIAGE_APPLY == 'true'` (enables scheduled enforcement) or
|
||||
a manual dispatch with `dry_run` unchecked. Merging the workflow alone never
|
||||
triggers a live run — review a few dry-run summaries first.
|
||||
|
||||
## Tokens
|
||||
|
||||
- CodeQL dismissals use the job `GITHUB_TOKEN` (`security-events: write`).
|
||||
- Dependabot dismissals and advisory creation need a repo/org secret
|
||||
**`SECURITY_TRIAGE_TOKEN`** (fine-grained PAT with *Dependabot alerts:
|
||||
write* + *Security advisories: write*) — `GITHUB_TOKEN` cannot do either.
|
||||
Without it the cron still classifies and reports; it just can't mutate
|
||||
Dependabot alerts or open advisories.
|
||||
|
||||
## Verified false positives (current backlog)
|
||||
|
||||
These were checked by reading the code during the initial audit and are safe to
|
||||
dismiss as false positives:
|
||||
|
||||
- `py/clear-text-logging-sensitive-data` @ `omnigent/inner/claude_sdk_executor.py`
|
||||
— the `logger.info` logs `model / gateway / base_url / tool-count`, no secret.
|
||||
- `py/weak-sensitive-data-hashing` @ `omnigent/model_catalog.py:225` — SHA256 is
|
||||
used to build a non-secret 16-char **cache fingerprint**, not to store a
|
||||
password. The secret is deliberately never persisted.
|
||||
|
||||
Accepted-risk (review, then dismiss with justification — not silently):
|
||||
|
||||
- `actions/untrusted-checkout` (critical) @ `oss-regen-on-comment.yml` — the
|
||||
`issue_comment` workflow checks out PR head, but with `persist-credentials:
|
||||
false`, no token on disk during `uv lock`, an App token minted only after the
|
||||
lock and used only at the push step, behind an `authorize` gate. Untrusted
|
||||
code runs without secrets in scope.
|
||||
|
||||
Needs per-case review (do **not** bulk-dismiss): the 52 `py/path-injection`
|
||||
findings in `spec/parser.py`, `tools/builtins/upload_file.py`, `spec/tar_utils.py`,
|
||||
etc. — most are trusted-input, but the extraction paths deserve a look.
|
||||
|
||||
Serious (fix, don't dismiss): `starlette` and `cryptography` advisories (server
|
||||
runtime); the `undici` cluster in `web`.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Custom semgrep rules for the contributor Security Scan (pass 1).
|
||||
# Run LOCALLY (semgrep --config this-file) so the scan needs no network to the
|
||||
# semgrep registry. These target code-execution / exfiltration shapes that an
|
||||
# untrusted PR might smuggle in; registry packs (p/ci, p/secrets) can be added
|
||||
# later as an additive, network-permitting step.
|
||||
rules:
|
||||
- id: exec-on-decoded-payload
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
message: >
|
||||
Executing a decoded/deobfuscated payload (base64/hex/zlib -> eval/exec).
|
||||
This is the canonical way to hide a backdoor from review.
|
||||
patterns:
|
||||
- pattern-either:
|
||||
- pattern: eval(...)
|
||||
- pattern: exec(...)
|
||||
- pattern-either:
|
||||
- pattern: eval(base64.$F(...))
|
||||
- pattern: exec(base64.$F(...))
|
||||
- pattern: eval(bytes.fromhex(...))
|
||||
- pattern: exec(bytes.fromhex(...))
|
||||
- pattern: eval(codecs.decode(...))
|
||||
- pattern: exec(codecs.decode(...))
|
||||
- pattern: eval(zlib.decompress(...))
|
||||
- pattern: exec(zlib.decompress(...))
|
||||
- pattern: eval($X.decode(...))
|
||||
- pattern: exec($X.decode(...))
|
||||
|
||||
- id: python-shell-pipe-to-interpreter
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
message: >
|
||||
A subprocess/os.system call pipes a downloaded script straight into a
|
||||
shell/interpreter (curl|wget ... | sh/bash/python). Runs arbitrary
|
||||
remote code.
|
||||
patterns:
|
||||
- pattern-either:
|
||||
- pattern: os.system($CMD)
|
||||
- pattern: os.popen($CMD)
|
||||
- pattern: subprocess.$F($CMD, ...)
|
||||
- pattern: subprocess.$F($CMD)
|
||||
- metavariable-regex:
|
||||
metavariable: $CMD
|
||||
regex: (?i).*(curl|wget)\b.*\|\s*(sudo\s+)?(sh|bash|zsh|python[0-9.]*|node|ruby|perl)\b.*
|
||||
|
||||
- id: shell-pipe-to-interpreter
|
||||
languages: [bash]
|
||||
severity: ERROR
|
||||
message: >
|
||||
Piping a downloaded script straight into a shell/interpreter. Runs
|
||||
arbitrary remote code in CI.
|
||||
patterns:
|
||||
- pattern-regex: (?i)(curl|wget)\b[^\n|]*\|\s*(sudo\s+)?(sh|bash|zsh|python[0-9.]*|node|ruby|perl)\b
|
||||
|
||||
- id: dynamic-import-from-network
|
||||
languages: [python]
|
||||
severity: WARNING
|
||||
message: >
|
||||
Dynamic import / module loading at runtime. Verify the source is trusted
|
||||
and not attacker-controlled.
|
||||
pattern-either:
|
||||
- pattern: importlib.import_module($X)
|
||||
- pattern: __import__($X)
|
||||
Reference in New Issue
Block a user