Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ jobs:
bash -n scripts/triage_scan.sh
bash -n benchmark/run_benchmark.sh

- name: Shellcheck (advisory)
# ubuntu-latest ships shellcheck preinstalled — no apt-get needed.
- name: Shellcheck (errors block the build; warnings are shown)
run: |
sudo apt-get update && sudo apt-get install -y shellcheck
shellcheck -S warning scripts/triage_scan.sh benchmark/run_benchmark.sh || true
shellcheck --severity=error scripts/triage_scan.sh benchmark/run_benchmark.sh
shellcheck --severity=warning scripts/triage_scan.sh benchmark/run_benchmark.sh \
|| echo "::warning::shellcheck reported non-error findings above"

- name: Dogfood — scan this repo with its own scanner
run: bash scripts/triage_scan.sh .
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ You point it at a repo, a skill, an MCP server, or a package, and it does an att

## How it works

The review has two layers. First, a read-only scanner does a fast pass over 13 categories of risk, from install hooks and obfuscated payloads to leaked secrets and hidden instructions. Then a five-persona adversarial read looks at what the scanner surfaced and reasons about intent, which is the part a keyword search can't do.
The review has two layers. First, a read-only scanner does a fast pass over 14 categories of risk, from install hooks and obfuscated payloads to leaked secrets and hidden instructions. Then a five-persona adversarial read looks at what the scanner surfaced and reasons about intent, which is the part a keyword search can't do.

The scanner never runs the code it reviews. It reads files and reports what it sees.
The scanner never runs the code it reviews — it only reads files. It also does no online research: the "search for current attack techniques" step happens in the full workflow, run by the agent or person, not by the shell script. Treat whatever that research pulls in as untrusted evidence, since web pages and repos can themselves carry prompt injection, not as instructions to act on.

## Why not just a signature scanner

Expand Down Expand Up @@ -105,7 +105,7 @@ Reviewing code is not the same as running it. Clone into a sandbox, don't instal
```
trust-issues/
├── SKILL.md the workflow: acquire safely → research attacks → scan → 5-persona read → verdict
├── scripts/triage_scan.sh read-only static triage, 13 categories
├── scripts/triage_scan.sh read-only static triage, 14 categories
├── references/ threat catalog + report template
├── benchmark/ labeled fixtures + recall harness
├── ARCHITECTURE.md how it's built and why
Expand Down
14 changes: 7 additions & 7 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,12 @@ do that? Note every gap between claimed purpose and actual capability.
```bash
bash scripts/triage_scan.sh <path-to-repo>
```
This is READ-ONLY (never executes the target). It surfaces candidates across 13
categories: inventory, binaries/blobs, npm install hooks, curl|bash, dynamic
exec/eval, base64/obfuscation, credential harvesting, network egress, leaked secrets,
CI/CD risk, dependency manifests, agent/prompt-injection directives, and hidden
unicode. Treat every hit as a lead to investigate, and remember a clean result proves
nothing on its own.
This is READ-ONLY (never executes the target) and does no online research. It surfaces
candidates across 14 categories: inventory, binaries/blobs, npm install hooks,
curl|bash, dynamic exec/eval, base64/obfuscation, credential harvesting, network egress,
leaked secrets, CI/CD risk, dependency manifests, agent/prompt-injection directives,
hidden unicode, and compiled-malware/injection/mining indicators. Treat every hit as a
lead to investigate, and remember a clean result proves nothing on its own.

### 4. Manual adversarial read through five personas
This is the core of the review — the scanner can't reason about intent, you can.
Expand Down Expand Up @@ -152,6 +152,6 @@ for manual follow-up; and the explicit verdict. Concentrated over exhaustive —
sharp one-page report beats a 200-item checklist.

## Resources
- `scripts/triage_scan.sh` — read-only static triage (13 categories).
- `scripts/triage_scan.sh` — read-only static triage (14 categories).
- `references/threat-catalog.md` — full threat taxonomy for the five personas.
- `references/report-template.md` — required report + verdict format.
128 changes: 80 additions & 48 deletions benchmark/run_benchmark.sh
Original file line number Diff line number Diff line change
@@ -1,99 +1,131 @@
#!/usr/bin/env bash
# run_benchmark.sh — measure what the triage scanner actually catches.
# run_benchmark.sh — measure what the triage scanner actually catches, and whether
# it catches it in the RIGHT category.
#
# Honest by design. The headline number is RECALL on known-malicious samples:
# of the malicious techniques in fixtures/, how many does the grep-based triage
# flag? We also report how many BENIGN samples surface a flag (the manual-review
# workload — high recall means some benign code gets surfaced, and that's the
# point of a triage layer, not a bug), and we spotlight any malicious sample that
# slips through, because "the scanner said nothing" is never proof of safety.
# Each malicious fixture declares the scanner category (section number) that should
# flag it. A fixture counts as caught only if it is flagged in one of its expected
# categories — not merely because the scan emitted "some output". That means a
# scanner that still prints noise but silently loses the rule that mattered will
# fail here. 'MISS' marks a fixture engineered to evade pattern matching; it is
# expected to slip through, and reporting that is the point.
#
# We also report how many BENIGN fixtures surface a flag (the manual-review
# workload — the scanner is tuned for recall; the reasoning pass clears these).
#
# Nothing here executes a fixture. Each sample is copied into a temp dir and
# scanned read-only.
#
# Usage: bash benchmark/run_benchmark.sh
# Usage: bash benchmark/run_benchmark.sh [--check]
# --check CI mode: exit 1 if any non-evasive fixture is missed or mis-categorized.

set -uo pipefail
CHECK=0
[[ "${1:-}" == "--check" ]] && CHECK=1 # CI mode: exit 1 if a NON-evasive malicious sample is missed
[[ "${1:-}" == "--check" ]] && CHECK=1
HERE="$(cd "$(dirname "$0")" && pwd)"
SCAN="$HERE/../scripts/triage_scan.sh"
MAL="$HERE/fixtures/malicious"
BEN="$HERE/fixtures/benign"

flagged() { # returns 0 (flagged) if the scan surfaces any signal for this sample
local sample="$1" tmp out sig
# fixture -> expected scanner category numbers (any one is sufficient).
declare -A EXPECT=(
[base64_exec.py]="5 6"
[curl_pipe_sh.sh]="4"
[dynamic_eval.js]="5"
[env_exfil.py]="7"
[hidden_unicode_SKILL.md]="12 13"
[miner_tor_c2.py]="14"
[npm_postinstall]="3"
[prompt_injection_SKILL.md]="12"
[reverse_shell.sh]="8"
[ssh_key_theft.py]="7"
[evasive_obfuscated.py]="MISS"
)

# sections_for SAMPLE: echo the space-separated category numbers whose section
# references the sample. Section 1 (inventory) is ignored because it lists paths.
sections_for(){
local sample="$1" name tmp out
name="$(basename "$sample")"
tmp="$(mktemp -d)"
cp -R "$sample" "$tmp/"
out="$(bash "$SCAN" "$tmp" 2>/dev/null)"
# look only at the signal sections (3..13), never the inventory/summary
sig="$(printf '%s\n' "$out" | sed -n '/== 3\./,/== SUMMARY/p')"
rm -rf "$tmp"
# a "hit" is any line that references the scanned temp path (path-printing
# sections) — inventory is excluded by the slice above
printf '%s\n' "$sig" | grep -q "$tmp"
printf '%s\n' "$out" | awk -v n="$name" '
/^== [0-9]+\./ { if (match($0, /[0-9]+/)) sec = substr($0, RSTART, RLENGTH) }
/^== 1\./ { sec = "" }
sec != "" && index($0, n) { print sec }
' | sort -un | tr "\n" " "
}

echo "############################################"
echo "# Trust Issues — benchmark #"
echo "############################################"
echo
printf "MALICIOUS FIXTURES (must be flagged in an expected category)\n"

printf "MALICIOUS FIXTURES (want: FLAGGED)\n"
printf " %-28s %s\n" "sample" "result"
mal_total=0 mal_hit=0 missed=""
mal_total=0 mal_ok=0 missed="" wrongcat=""
for s in "$MAL"/*; do
mal_total=$((mal_total+1))
name="$(basename "$s")"
if flagged "$s"; then
printf " %-28s \033[32mFLAGGED\033[0m\n" "$name"; mal_hit=$((mal_hit+1))
expect="${EXPECT[$name]:-}"
hits="$(sections_for "$s")"
if [[ "$expect" == "MISS" ]]; then
if [[ -n "${hits// /}" ]]; then
printf " %-28s evasive — unexpectedly caught in %s\n" "$name" "$hits"
else
printf " %-28s MISS (by design)\n" "$name"
fi
continue
fi
mal_total=$((mal_total + 1))
ok=0
read -ra exp_arr <<< "$expect"
for e in "${exp_arr[@]}"; do
case " $hits " in *" $e "*) ok=1 ;; esac
done
if (( ok )); then
mal_ok=$((mal_ok + 1)); printf " %-28s OK (category %s)\n" "$name" "$expect"
elif [[ -n "${hits// /}" ]]; then
wrongcat="$wrongcat $name"; printf " %-28s WRONG CATEGORY (hit %s, expected %s)\n" "$name" "$hits" "$expect"
else
printf " %-28s \033[31mMISSED\033[0m\n" "$name"; missed="$missed $name"
missed="$missed $name"; printf " %-28s MISSED entirely\n" "$name"
fi
done

echo
printf "BENIGN FIXTURES (flag = surfaced for manual review)\n"
printf " %-28s %s\n" "sample" "result"
printf "BENIGN FIXTURES (a flag = surfaced for manual review, not a failure)\n"
ben_total=0 ben_flag=0 noisy=""
for s in "$BEN"/*; do
ben_total=$((ben_total+1))
name="$(basename "$s")"
if flagged "$s"; then
printf " %-28s \033[33mSURFACED\033[0m\n" "$name"; ben_flag=$((ben_flag+1)); noisy="$noisy $name"
ben_total=$((ben_total + 1))
if [[ -n "$(sections_for "$s" | tr -d ' ')" ]]; then
ben_flag=$((ben_flag + 1)); noisy="$noisy $name"; printf " %-28s surfaced\n" "$name"
else
printf " %-28s quiet\n" "$name"
fi
done

recall=$(( mal_total ? 100*mal_hit/mal_total : 0 ))
echo
echo "--------------------------------------------"
echo "RESULTS"
echo " Malicious recall: $mal_hit / $mal_total (${recall}%)"
echo " Missed (evasion): ${missed:- none}"
echo " Benign surfaced: $ben_flag / $ben_total for manual review:${noisy:- none}"
echo " Correct-category recall: $mal_ok / $mal_total"
echo " Missed entirely: ${missed:- none}"
echo " Wrong category: ${wrongcat:- none}"
echo " Benign surfaced: $ben_flag / $ben_total for review:${noisy:- none}"
echo "--------------------------------------------"
cat <<'NOTE'
How to read these numbers:
- Recall below 100% is expected. The evasive fixture is built to defeat pattern
matching, the same way real scanner-evasion works, so missing it is a designed
outcome that argues for the manual five-persona read and for sandboxing.
- "Benign surfaced" is the manual-review workload rather than a failure. The scanner
is tuned for recall, and the reasoning pass clears the legitimate cases such as a
scheduler using spawnSync, a regex calling .exec(), or a config reading one env var.
- A clean scan on its own does not clear the code.
How to read this:
- "Correct-category recall" is stronger than "emitted some output": a fixture only
counts if flagged in the category matching its technique.
- "Wrong category" means the scanner noticed something but lost the specific rule —
treat that as a regression to fix, not a pass.
- The evasive fixture is expected to be missed; that is the argument for the manual
five-persona read and for sandboxing. A clean scan never clears the code.
NOTE

# CI gate: fail only if a malicious sample that is NOT meant to be evasive was missed.
if [[ "$CHECK" == "1" ]]; then
unexpected=""
for m in $missed; do
case "$m" in *evasive*) : ;; *) unexpected="$unexpected $m" ;; esac
done
if [[ -n "$unexpected" ]]; then
echo; echo "CI FAIL: unexpected miss(es):$unexpected" >&2
if [[ -n "${missed// /}" || -n "${wrongcat// /}" ]]; then
echo; echo "CI FAIL: missed:${missed:- none} | wrong-category:${wrongcat:- none}" >&2
exit 1
fi
echo; echo "CI OK: no unexpected misses."
echo; echo "CI OK: every non-evasive fixture flagged in its expected category."
fi
Loading
Loading