Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ jobs:
run: bash scripts/tests/test_telegram_route.sh
- name: capability mode tests
run: bash scripts/tests/test_skill_mode.sh
- name: vuln-scanner status contract tests
run: python3 scripts/tests/test_vuln_scanner_status.py
- name: per-skill requires parse tests
run: bash scripts/tests/test_skill_requires.sh
- name: run actions summary tests
Expand Down
8 changes: 4 additions & 4 deletions eyebrowlock.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
"files": [
{
"path": "SKILL.md",
"hash": "b17491de609e1fa3b241a55012e60f21d63eaaac6ad9251c43509f40ec635872"
"hash": "df58bcc453fbd2d185b3fc1f616c36babe1a0f598a3ec22980e69958dcf475e2"
}
],
"findings": [
Expand All @@ -144,7 +144,7 @@
"severity": "medium",
"owasp": "ASK-03",
"file": "SKILL.md",
"line": 304,
"line": 324,
"snippet": "- `exec` / `spawn` / `system` / `eval` sinks + subprocess with string interpolation (RCE)",
"explanation": "uses a process-execution primitive"
},
Expand All @@ -153,12 +153,12 @@
"severity": "critical",
"owasp": "ASK-01",
"file": "SKILL.md",
"line": 1013,
"line": 1053,
"snippet": "1. **Install** — the binaries (`semgrep`, `trufflehog`, `osv-scanner`, `slither`) are **not pre-installed**. Stage the…",
"explanation": "downloads and executes remote code via a pipe to a shell"
}
],
"contentHash": "sha256-a81f0757d7b3bafe3baaa0c3d2c0e735eb284a7ebc9768264bdfe4e747b4aa9a",
"contentHash": "sha256-c990e0d6823aac253f655cf6c4640606d6f351efa088ccc9f508f3d02723575c",
"discoveredFrom": "skills/vuln-scanner/SKILL.md"
},
{
Expand Down
3 changes: 3 additions & 0 deletions scripts/skill_mode.sh
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ WRITE_TOOLS="Write,Edit,Bash(gh:*),Bash(git:*),Bash(python3:*),Bash(python:*)"
# a live-test showed the run logging that denial as "Blocked by sandbox". These are
# read-only static-analysis tools (no repo/network mutation of their own).
WRITE_TOOLS="$WRITE_TOOLS,Bash(semgrep:*),Bash(osv-scanner:*),Bash(trufflehog:*),Bash(slither:*)"
# Bounded scanner calls start with the wrapper, not the scanner's bare name.
# These can execute arbitrary commands, so grant them only in the write tier.
WRITE_TOOLS="$WRITE_TOOLS,Bash(timeout:*),Bash(gtimeout:*)"
# cargo (vuln-scanner Arm A, step A3.5 — dynamic testing). Staged by
# scripts/stage-vuln-scanner.sh (nightly toolchain + cargo-fuzz, workflow step,
# same reason as Foundry below — the sandbox denies toolchain installs in-run).
Expand Down
11 changes: 11 additions & 0 deletions scripts/tests/test_skill_mode.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ echo "$WT" | grep -q "Write" && echo "$WT" | grep -q "Edit" \

# allowed-tools: read-only tier drops mutation tools but keeps read+notify+curl
RT=$(bash "$M" allowed-tools read-only)
# Timeout wrappers are command heads, not covered by Bash(trufflehog:*).
# Keep these general command runners out of the read-only tier.
for timer in timeout gtimeout; do
echo "$WT" | tr ',' '\n' | grep -qxF "Bash($timer:*)" \
&& pass "write tier includes $timer wrapper" || bad "write tier missing $timer wrapper"
if echo "$RT" | tr ',' '\n' | grep -qxF "Bash($timer:*)"; then
bad "read-only tier exposes $timer wrapper"
else
pass "read-only tier excludes $timer wrapper"
fi
done
if echo "$RT" | grep -q "Write" || echo "$RT" | grep -q "Edit" \
|| echo "$RT" | grep -q "Bash(git:\*)" || echo "$RT" | grep -q "Bash(gh:\*)"; then
bad "read-only tier drops Write/Edit/git/gh"
Expand Down
27 changes: 27 additions & 0 deletions scripts/tests/test_vuln_scanner_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Contract checks, not a claim that a model followed the reporting instructions."""
from pathlib import Path
import unittest

SKILL = (Path(__file__).resolve().parents[2] / "skills/vuln-scanner/SKILL.md").read_text()


class ScannerStatusContract(unittest.TestCase):
def test_report_preserves_history_status(self):
report = SKILL.split("### A7. Write local report", 1)[1].split("### A8.", 1)[0]
self.assertIn("trufflehog-git", report)
self.assertIn("sources.txt", report)

def test_both_notification_templates_preserve_history_status(self):
notify = SKILL.split("### A8. Notify", 1)[1].split("## Arm D", 1)[0]
rows = [line for line in notify.splitlines() if "Scanners:" in line]
self.assertEqual(len(rows), 2)
for row in rows:
self.assertIn("trufflehog-git=<ok|fail|timeout>", row)

def test_log_preserves_history_status(self):
log = SKILL.split("## Log", 1)[1].split("## Network note", 1)[0]
self.assertIn("trufflehog-git=ok|fail|timeout", log)


if __name__ == "__main__":
unittest.main()
56 changes: 48 additions & 8 deletions skills/vuln-scanner/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,20 @@ fi
if command -v trufflehog >/dev/null 2>&1; then
trufflehog filesystem . --only-verified --json \
> /tmp/vuln-scan/trufflehog.json 2>/dev/null || true
# Also scan full git history for secrets
trufflehog git file://. --only-verified --json \
> /tmp/vuln-scan/trufflehog-git.json 2>/dev/null || true
# Also scan full git history for secrets — BOUNDED. An unbounded `trufflehog git`
# walks every commit's every tree, and a large packed history (measured: 200
# commits / ~369MB on one real run) can eat the whole turn budget by itself,
# with nothing to show it happened until the run reports "success" anyway
# having produced no report at all. `timeout` turns that silent budget-burn
# into an ordinary, honestly-recorded `fail` — same as an install failure,
# never a reason to write a "still running, will resume" placeholder as the
# final output. There is no resume: a workflow_dispatch run is one shot, and
# a note promising to pick back up later is not truthful about what a single
# run can actually do.
timeout 300 trufflehog git file://. --only-verified --json \
> /tmp/vuln-scan/trufflehog-git.json 2>/dev/null
TRUFFLEHOG_GIT_RC=$?
[ "$TRUFFLEHOG_GIT_RC" = 124 ] && echo "VULN_SCANNER_TIMEOUT: trufflehog git history scan exceeded 300s on a large packed history — recorded as fail, not retried, not left unfinished"
else
echo "VULN_SCANNER_SKIPPED: trufflehog not available"
fi
Expand Down Expand Up @@ -181,6 +192,15 @@ fi
# Record what succeeded (empty output ≠ clean, could be tool failure)
echo "semgrep=$([ -s /tmp/vuln-scan/semgrep.json ] && echo ok || echo fail)" > /tmp/vuln-scan/sources.txt
echo "trufflehog=$([ -s /tmp/vuln-scan/trufflehog.json ] && echo ok || echo fail)" >> /tmp/vuln-scan/sources.txt
# Recorded separately from the filesystem pass above: they can genuinely diverge
# (filesystem scan clean and fast, git-history scan timed out on a large packed
# repo, or vice versa) and collapsing both into one trufflehog= line hides
# whichever one actually failed.
if [ "${TRUFFLEHOG_GIT_RC:-1}" = 124 ]; then
echo "trufflehog-git=timeout" >> /tmp/vuln-scan/sources.txt

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[ISSUE] trufflehog-git=timeout is not represented in any of the prescribed A7/A8/log status formats (which still expose only trufflehog=<ok|fail>), and timeout is outside their declared ok|fail vocabulary — why it matters: a clean filesystem pass can still be reported as trufflehog=ok while the timed-out history pass disappears from the durable report and operator notification, preserving the misleading-success failure this change is intended to fix.

else
echo "trufflehog-git=$([ -s /tmp/vuln-scan/trufflehog-git.json ] && echo ok || echo fail)" >> /tmp/vuln-scan/sources.txt

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[ISSUE] Determine success from the TruffleHog exit code, not whether its JSON output is non-empty. A successful clean trufflehog git --json scan emits no finding records, so this branch records trufflehog-git=fail and forces a “limited audit” for the normal zero-secret case.

fi
echo "osv=${OSV_STATUS:-fail}" >> /tmp/vuln-scan/sources.txt
```

Expand Down Expand Up @@ -605,7 +625,25 @@ Append to `memory/vuln-scanned.json` (create if missing) so future runs skip thi

### A7. Write local report

Save to `output/articles/vuln-scan-${today}.md` with sections for: repo metadata, scanner sources (ok/fail per tool), candidate count, confirmed findings with severity and channel, PoC gate status (`verified` with verifier/chain/block, `not-required` with reason, or `needs-verification`), and dedup note. Do **not** include exploit details for findings disclosed via PVR — redact file/line and link to the advisory ID instead.
**There is no resume.** The git-history pass has a timeout; this does not bound
every other scanner or installation step. If you are running low on turns, finish
the report with whatever scanners actually completed, record the rest `fail` in
`sources.txt` (§A3's rule: unfinished is `fail`, not a pending state), and write
A7/A8 now. A single `workflow_dispatch` run is one shot with no continuation —
writing "still running, will pick this up automatically" as the final output is
not true of this run path (it was live-observed: a run reported workflow
`success` having written that sentence instead of a report, with no ledger entry
at all — the operator had to notice and re-dispatch by hand). A shorter, honest
report with some scanners marked `fail` is a completed task; a promise to
resume is not.

Save to `output/articles/vuln-scan-${today}.md` with sections for: repo metadata, scanner sources (ok/fail per tool — `trufflehog` and `trufflehog-git` are two separate rows, not one; folding a timed-out history scan into a clean filesystem-scan's `ok` is exactly the silent-masking this split exists to prevent), candidate count, confirmed findings with severity and channel, PoC gate status (`verified` with verifier/chain/block, `not-required` with reason, or `needs-verification`), and dedup note. Do **not** include exploit details for findings disclosed via PVR — redact file/line and link to the advisory ID instead.

Copy each scanner status from `sources.txt` into the report, notification and log.
Keep `trufflehog` (filesystem) and `trufflehog-git` (history) separate, preserving
`timeout` exactly. Missing status is `fail`, never inferred `ok`. If any pass failed
or timed out, say "limited audit" and name the incomplete coverage, even when zero
findings were confirmed. Do not describe incomplete coverage as a clean audit.

### A8. Notify

Expand All @@ -615,13 +653,15 @@ Use `./notify`. One paragraph. Lead with the verdict.
*Vuln Scanner — <repo>*
<N> confirmed findings (<severity-summary>).
Disclosed via: <PVR: advisory #123 | public PR #45 | skipped (no channel)>
Scanners: semgrep=<ok|fail>, trufflehog=<ok|fail>, osv=<ok|fail>, fuzz=<ok|fail|skip>. PoC gate: <verified|not-required|needs-verification>.
Scanners: semgrep=<ok|fail>, trufflehog=<ok|fail>, trufflehog-git=<ok|fail|timeout>, osv=<ok|fail>, fuzz=<ok|fail|skip>. PoC gate: <verified|not-required|needs-verification>.
```

If the audit was clean:
`trufflehog-git=timeout` must always be spelled out here, never folded into a plain `trufflehog=ok` — a clean filesystem pass and a timed-out history pass are different facts, and this is the durable line an operator actually reads. Silently dropping the git-history state here reproduces the exact masking this field exists to prevent.

If no findings were confirmed (choose clean or limited according to actual coverage):
```
*Vuln Scanner — <repo>*
Clean audit. <M> candidates reviewed, 0 confirmed. Scanners: semgrep=ok, trufflehog=ok, osv=ok, fuzz=skip, agentic=ok.
<Clean audit | Limited audit — name incomplete passes>. <M> candidates reviewed, 0 confirmed. Scanners: semgrep=<ok|fail>, trufflehog=<ok|fail>, trufflehog-git=<ok|fail|timeout>, osv=<ok|fail|none|skipped>, fuzz=<ok|fail|skip>, agentic=<ok|skipped>.
```

Then log per the **Log** section below with `Mode: scan`.
Expand Down Expand Up @@ -983,7 +1023,7 @@ specific bullets.
- Candidates: N | Confirmed: M
- Channels used: PVR (x), public PR (y), skipped (z)
- Prior-art check: N candidates checked, 0 matches | matched #123 → skipped/commented
- Scanner status: semgrep=ok trufflehog=ok osv=ok fuzz=ok|fail|skip agentic=ok|skip poc=verified|not-required|needs-verification
- Scanner status: semgrep=ok|fail trufflehog=ok|fail trufflehog-git=ok|fail|timeout osv=ok|fail|none|skipped fuzz=ok|fail|skip agentic=ok|skip poc=verified|not-required|needs-verification
- Advisory/PR links: [...]
```

Expand Down