12 Commits

Author SHA1 Message Date
kz-tob 9e3fd2f9e8 constant-time-analysis: pair ilspycmd's TFM with a matching runtime (#283)
* constant-time-analysis: pair ilspycmd's TFM with a matching runtime

The tool-store fallback in _get_il_output globbed for
`*/ilspycmd/*/tools/net8.0/any/ilspycmd.dll`. ilspycmd 9.x installs
under `tools/net9.0/`, so on any newer install the glob matched
nothing, the loop body never ran, and C# IL analysis fell through to
monodis and then to "IL disassembly tools not available" — while
`dotnet tool install -g ilspycmd`, the command that error recommends,
produces exactly the layout the glob could not see.

Widening the glob to net* alone would have been wrong. The next lines
run the assembly under a Homebrew dotnet@8 runtime, and that pairing is
deliberate: ilspycmd installs framework-dependent and .NET does not
roll forward across a major version by default, so a net9.0 assembly
will not start on an 8.0 runtime. A wide glob with a pinned runtime
turns a silent no-match into a silent failed exec.

So the TFM found on disk now selects the runtime. Candidates are tried
newest-first, sorting the parsed (major, minor) tuple rather than the
moniker string, since lexically "net10.0" sorts below "net8.0"; a TFM
whose runtime is absent falls back to an older one that has one. Store
entries that do not name a .NET runtime major (netstandard2.0, net48)
are skipped rather than producing a dotnet@netstandard2 path. The
arbitrary `break` after the first matching dll is gone.

Extracted to _il_via_versioned_runtime so _get_il_output stays flat.

TestCSharpILRuntimePairing covers it with a fake store and fake runtime
paths, needing no dotnet install. The suite was mutation-tested rather
than trusted green: against the old pinned glob two tests fail, and
against the naive widening four fail, including the one asserting a
net9.0 assembly is never handed to dotnet@8.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* constant-time-analysis: don't depend on one Homebrew keg existing

Review P2 on #283: pairing the store TFM with /opt/homebrew/opt/dotnet@N
assumes a keg for that major exists, and if it does not the motivating
failure just moves from the glob to the exists() check.

`brew info dotnet@9` says the keg does exist — stable 9.0.120 — so the
ilspycmd 9.x case the PR was written for does work as shipped. The
general concern holds anyway: homebrew-core carries dotnet@6, dotnet@8
and dotnet@9 but dropped dotnet@7 at end of life, and no Linux
distribution ships a versioned keg at all.

So the exact-major keg is now preferred rather than required. After it
come the unversioned Homebrew prefixes, the official installer paths for
macOS and Linux, and the PATH dotnet, each tried with
DOTNET_ROLL_FORWARD=Major, which is what permits a net9.0 assembly to
start on a 10.x runtime. Roll-forward is set only on those candidates:
setting it on the exact-major keg would mask a genuinely mismatched pair
instead of letting it fail, which is the property the existing test
pins. The PATH entry resolves through shutil.which, since dotnet_path
defaults to the bare name "dotnet" and exists() on a bare name tests the
cwd.

The except clause widens from FileNotFoundError to OSError. This is P3,
included because the change makes it reachable: adding the Intel prefixes
means an Intel keg on an Apple Silicon box without Rosetta is now a live
candidate, and it raises OSError("Bad CPU type in executable") rather
than FileNotFoundError, which escaped the helper and aborted the whole
analysis.

Review P2 on references/vm-compiled.md: the only user-facing remedy still
said `brew install dotnet@8` and claimed the analyzer detects dotnet@8.
It now tells the reader to read the TFM out of the tool store, names the
keg gap, and explains that the roll-forward path means no install is
strictly required. The store lookup uses find rather than ls on a glob,
for the reason #282 documents.

Five tests added, each mutation-checked: dropping the generic tier,
setting roll-forward unconditionally, narrowing OSError back, and
deleting the DOTNET_ROOT line each fail exactly the test that covers
them. DOTNET_ROOT had no assertion before, so that line could be deleted
with the suite green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 11:05:27 -04:00
kz-tob 4b1b74b181 Give differential-review a trigger, and name every component in its README (#278)
* Give differential-review a trigger, and name every component in its README

differential-review's description listed what it does and never named a
situation, so it competed on capability wording alone. It now closes with
the triggers its own README already documents — reviewing a PR, commit,
or diff; checking whether a change re-introduces a fixed bug; asking what
else a change could break; finding modified code with no test.

The same plugin's README never mentioned adversarial-modeler, which is
what Phase 5 dispatches for HIGH RISK changes. Checking whether that was
isolated turned up more of it, and the sweep found three kinds of gap:

zeroize-audit's agent table was missing three of its eleven agents —
0-preflight, which gates the entire run, plus 5b-poc-validator and
5c-poc-verifier. All three appear in the phase diagram directly above the
table, which is why they read as present.

constant-time-analysis documents the ct-analyzer CLI end to end and never
says the plugin also ships a skill and a command. entry-point-analyzer
lists phrases that trigger its skill but never names the skill or its
command.

Three more READMEs describe their skill without naming it. That matters
most where the skill name is not the plugin name and a user cannot guess
it: chrome-mcp-troubleshooting and interpreting-culture-index.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fix review findings and make the README sweep a gate

The two PoC rows I added to zeroize-audit said Phase 4. The diagram three
lines above them, SKILL.md, and workflows/phase-5-poc-validation.md all
say Phase 5, steps 5a and 5b. "Wave 5a" is a label that exists nowhere.
A debugger consulting the table — the artifact this branch designates as
what runs when — would have opened phase-4-poc-generation.md and found no
validation in it. Also corrected the sentence introducing that table,
which still said 10 agents across 8 phases against 11 across 9, and the
Phase 0 diagram line, which still credited the orchestrator for a gate
the new row credits to 0-preflight.

differential-review's README claimed the agent is "dispatched", and named
it bare in a column whose other rows are namespaced. Nothing dispatches
it: the only instruction is prose in SKILL.md, and a bare subagent_type
fails at runtime. Namespaced both, and corrected the five stale line
counts in the same file — reporting.md is 369 lines, not the ~120 the
token-efficiency section budgets for.

Drop the dead `name: trailofbits:<cmd>` key from five command files.
The three newest command files carry no name: at all, #275 namespaced 22
bare invocations, and this branch documents the `/<plugin>:<cmd>` form —
so the key contradicts the docs it sits next to.

Then make the sweep repeatable. Doing this by hand three times found
eight gaps and missed two more, both of the same shape: a workflow ships
under meta.name, not its filename, so a README citing the filename never
writes the name a reader types. The validator now checks that a README
names every skill, agent, command, and workflow its plugin ships, reading
meta.name for workflows. It refuses a run that inspected zero components,
and six self-test assertions hold it to known-bad fixtures.

It found git-cleanup on its first run: ships as /git-cleanup:git-cleanup-analysis,
README cites workflows/analyze-branches.js four times and that name never.
static-analysis had the same gap for codeql-build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fix both P2s: the gate was a substring test, and the dispatch was still bare

The README gate ran `name not in text`. That reads as thorough and could
not fail for a large share of what it counted: `draw` was satisfied by
"(draw cards instead)", `semgrep-rule` by the plugin's own name in the
install line, `burp-search` by a `scripts/burp-search.sh` path that is a
different thing, and `audit` by the prose "shared-state struct audit".

Match by kind instead. Commands and workflows are reachable only as
`/<plugin>:<name>`, so require that literal — it is the only string a
user can type. Agents are dispatched by identifier and never typed as
prose, so require an identifier-shaped mention. Skills are genuinely
referred to by bare name, so require only a delimited occurrence, which
is what stops "draws" counting as `draw`.

That surfaced seven real gaps, the four above plus insecure-defaults'
audit-pipeline workflow, mutation-testing's skill, and trailmark's
code-slice-worker. All seven fixed.

adversarial-modeler was still bare at SKILL.md:96. Line 77 was the
decision-tree mention; line 96 is the "Delegate to this agent"
instruction a model actually acts on, so the runtime failure the last
commit claimed to fix survived it. Namespaced, and it now says why.

Also from the review: a per-kind floor, since a single total stays
healthy while skill_files() — 63% of coverage — silently stops matching;
workflow_names anchored to the meta block, because a bare search takes
any earlier `name:` in a comment, and .mjs was invisible; and AGENTS.md
documents the new hard failure. Self-test 88 -> 96, each new rule with a
negative control.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 08:54:12 -04:00
kz-tob 8c1b2670bf constant-time-analysis: make the VLD version lookup portable (#277)
Both copies of the PECL install snippet ran

  VLD_VERSION=$(curl -s ... | grep -oP 'vld-\K[0-9.]+(?=\.tgz)' | head -1)

PCRE mode is a GNU extension. Under stock macOS grep this exits 2, the
command substitution yields an empty string, and the next line installs
a package named `vld-` with no version at all:

  grep: invalid option -- P
  VLD_VERSION=[]
  would run: pecl install channel://pecl.php.net/vld-

Three things let that happen, so fix all three. POSIX ERE instead of
PCRE, keeping the -o semantics through a sed strip. curl -fsS so an HTTP
or DNS failure is reported rather than feeding empty input downstream.
And a guard, so the install runs only when a version was actually found
and otherwise hands the reader the URL to check by hand.

Verified against the live PECL page: old and new both return 0.19.1, so
behaviour is unchanged where the old form worked at all. Both snippets
were then executed with PATH=/usr/bin:/bin to force BSD tools, on the
success path and with PECL unreachable.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 11:50:11 -04:00
Dan Guido 9e06dc67a3 Make every documented command runnable under our own python shims (#258)
* Make every documented command runnable under our own python shims

The modern-python plugin ships PATH shims that refuse `python <script>`,
`pip install`, `python -m pip` and `uv pip install`. Twelve other plugins
in this marketplace issued exactly those forms, so installing our own
plugin broke our own skills — and CI was green throughout.

The worst case was not theoretical. c-review and rust-review both call
their Phase 4 planner as `python3 "${PLUGIN_ROOT}/scripts/build_run_plan.py"`,
so with the shim installed every run died before spawning a worker.
Verified both directions: the new form exits 0 with the shim on PATH, the
old form exits 1.

Phase 1's reading pass named 16 skills. A mechanical sweep found 96
candidate lines across 44 files, and scanning shell scripts as well as
markdown found 10 more the docs sweep had missed. That gap is the reason
the check below exists.

The fix is not one substitution. Four classes needed different treatment:

- Our own scripts become `uv run --no-project <script>`. Not bare `uv run`,
  because these execute inside the *target* repo, which may be a Python
  project that cannot resolve; verified against a broken pyproject.toml and
  against validate_artifacts.py's sibling import of generate_sarif.
- Package installs become `uv add` for a dependency, `uv tool install` for a
  CLI, `uv sync` for a project's own editable install.
- Third-party CLIs we merely document — OSS-Fuzz's infra/helper.py, yarGen —
  become `uv run --no-project python <script>`, which keeps upstream's exact
  semantics rather than handing their script an environment we manage.
- atheris's instrumented build keeps its source build, as
  `uv add --no-binary-package cbor2`. Dropping that flag would silently
  produce an uninstrumented fuzzer, which is worse than a visible failure.
  Its prose was updated to name the flag it now uses.

Two factual corrections fell out. `pip install caracal` was wrong twice
over: caracal is a Rust tool (Cargo.toml at its root), so it is now
upstream's own `cargo install --git`, not a uv equivalent that would fetch
an unrelated PyPI package. And `pip install uv` cannot bootstrap uv under
a shim that intercepts pip, so culture-index now points at the official
installer.

Thirteen lines stay as they are, each deliberately: Dockerfile `RUN` lines
and oss-fuzz's build.sh run in containers where our shims are absent;
codeql's pip calls install the *analysed* project's dependencies, and that
project is arbitrary; trailmark's dispatch skills must keep saying "Do NOT
run `pip install`"; and modern-python documents what it intercepts.

`make shell-suites` passes again as a result — exit 0 with the 1.6.0 shim,
where AGENTS.md previously recorded it as broken by variant-analysis.

The guardrail: check_python_invocations scans 698 markdown and shell files
and fails on the four refused forms, with structural exemptions for
dockerfile fences and an `allow-legacy-python: <reason>` marker that scopes
to its code block. Eleven self-test fixtures cover it, four asserting it
fires and seven asserting it stays quiet on the compliant forms. It was
mutation-tested in both languages, and it caught its own worst bug during
development: unanchored patterns first flagged `uv run --no-project python
fuzz.py`, the very form the advice recommends. Self-test goes 45 -> 56.

* Review pass: fix the atheris flow, drop a stray exemption, trim comments

Three corrections from reviewing the branch diff:

- atheris's install now opens with `uv init --bare`, without which the
  documented `uv add atheris` errors in a bare harness directory. The old
  pip form assumed an activated venv, so setup was always implicit; now
  it is one explicit line.
- ossfuzz carried an allow-legacy-python marker on a C++ build block that
  contains no python at all — yesterday's insertion matched the first of
  three "Build in build.sh" headings instead of the python one. The
  exemption now sits only on the block that needs it.
- The anti-vacuity message said "read no markdown" for a scan that also
  covers shell scripts.

The rest is weight: the new check's comment blocks, the hardcoded-path
constants' commentary, the AGENTS.md bullets and the three exemption
markers all said the same things at two to three times the length. Each
keeps its one-line why; the narratives are gone. No behavioural change —
self-test still passes 56 assertions and the full scan is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Address the review: fix where packages land, widen the check to match the shim

The review's core insight was right twice over. Several substitutions had
changed WHERE a package lands, breaking the documented next step, and the
checker enforced a narrower invariant than the shim it exists to mirror.

Where packages land:

- trailmark is imported as a library from five skills, and a `uv tool
  install` environment is not importable — the retry loop at
  trailmark/SKILL.md:47-51 would have spun forever on the exact error it
  names. The CLI install stays `uv tool install`; the import snippets now
  run under `uv run --with trailmark python -`.
- `uv add` writes to the manifest of whatever project you are standing
  in, which for sarif-parsing is the audited repo. Its scripting rows,
  ijson comment and jsonschema example now use `uv run --with <pkg>`,
  which leaves no trace. atheris keeps `uv add` deliberately: the fuzzing
  harness is the user's own project, made explicit by `uv init --bare`.
- `uv sync` leaves ct-analyzer in .venv/bin, so the README's very next
  line failed with command not found. Now `uv tool install .`, verified
  end to end: the console script lands on PATH and --help runs.
- yarGen needs pefile/lxml/yara-python, which `--no-project` had detached;
  now `uv run --with-requirements requirements.txt`.
- The cbor2 source-build preference now persists via
  `no-binary-package = ["cbor2"]` under [tool.uv] (field verified against
  uv's accepted-settings list), so a later `uv sync` cannot silently swap
  in an uninstrumented wheel.

The checker, widened to the shim's actual behaviour:

- `python3 --version` and `python3 -u foo.py` are refused by the shim but
  passed the old patterns; one live instance (constant-time-analysis
  README) proved it. Both forms are now caught.
- Every `uv pip` subcommand is refused, not just install; `-t` joins the
  allowed tool-managed flags.
- .py files are scanned too: usage strings and error messages told users
  to run refused commands from ten scripts, including the --help of the
  very planner this PR fixed. All rewritten.
- The evals/tests exemption now tests path parts relative to plugins/, so
  a checkout under a directory named tests no longer exempts every file.
- An allow-marker's scope ends at a blank line as well as a fence, so one
  marker cannot blanket a whole file; quality-assessment.md gains the
  second marker that scoping made necessary.

Also from the review: zeroize's preflight gets `which python3` back (a
helper script still needs the binary; the shim never required removing
it), the Makefile's shell-suites note no longer describes an interception
that is gone, and the cairo CI example warns that it rebuilds caracal
from source each run.

Self-test 56 -> 63; every new pattern and exemption is fixture-covered
and was mutation-probed against the real tree. Full scan: 0 findings over
773 files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Address the second review: prerequisite probe, checker parity, package placement

The review's P2 was a regression this PR introduced for a population the
first fix ignored: c-review and rust-review now require uv, and a box
with python3 but no uv would die at Phase 4 exactly the way shimmed boxes
died before. Phase 1 (Prerequisites) in both skills now probes
`command -v uv` and aborts with install guidance. zeroize-audit's
preflight already checked uv. The four converted shell suites gain the
same guard with a clear message instead of a bare 127 mid-run.

Checker parity with the shims, second pass:

- pipx and the non-install pip subcommands are refused by catch-all shim
  arms and passed the checker; both get named-subcommand patterns.
- A script named by variable or path (`python3 "$MERGE"`) has no `.py`
  token; a new pattern covers it and immediately caught one live
  instance — a codeql test stub that fakes uv itself, now carrying an
  allow-marker with its reason.
- finditer everywhere: a compliant `uv run` earlier on a line no longer
  masks a refused command later on it, which was exactly the table-cell
  case the unanchored design exists for.
- Prohibition phrases now test the text BEFORE the match, so
  "Use `pip install semgrep` instead of the tarball" is flagged while
  "Do NOT run `pip install`" stays exempt.
- The uv-pip allowance matches whole flags after the command, so
  `--target-dir` no longer counts as `--target` and a trailing `-t /tmp`
  does; `uv pip` precedes `pip` in the pattern order so its lines get
  the right advice; a pip match directly after `uv ` defers to the
  uv-pip verdict instead of double-reporting.

Package placement, continued from the same insight as round one:

- yarGen regains --no-project alongside --with-requirements, plus a cd
  into the checkout so requirements.txt resolves where it lives.
- sarif-parsing's jsonschema example no longer names a script that does
  not exist, and the table's run-forms show a concrete script.py.
- culture-index's two messages now agree and name the actual remedy
  (`uv run --project` on the scripts directory) instead of re-adding a
  dependency its pyproject already declares.
- merge_sarif's usage line gains --no-project; the generator plugin's
  install section stops prescribing a venv its own runner never uses.
- generate_poc declared requires-python >=3.9 while using `str | None`
  in a signature, a TypeError on 3.9 that uv's interpreter selection
  made reachable; now >=3.10.
- The GitLab CI example exports ~/.local/bin onto PATH, without which
  `uv tool install` warns and the next line dies command-not-found.

Self-test 63 -> 71; the masking, prohibition-direction, flag-position
and pipx cases are all fixtures, and each new pattern was probed live
against the tree (plant, error, remove, clean — 0 findings over 773
files).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Address the third review: importable trailmark, honest probes, sturdier scan

The P2 was the residue of round two's own fix, applied to the siblings
but not the flagship: trailmark/SKILL.md told the model to cure an import
error with `uv tool install`, which cannot cure it — a tool env is not
importable — while forbidding every fallback. The install block now says
what each remedy is for: `uv tool install` for the CLI, `uv run --with
trailmark python -` for the snippets, and the other five library-first
docs carry the same one-line annotation next to their install command.

Empirically settled rather than taken from the review: `uv run python3
<script>` works fine under the shims — uv prepends its environment's bin
directory, so python3 resolves to a real interpreter, not the shim. The
review's claim to the contrary would have meant rewriting the Makefile
and a bats suite; a two-minute transcript said no. Also declined: a
zeroize uv-prerequisite (its preflight already lists uv and uvx; the
C/C++ `which` line now names uv too).

Real and fixed:

- ct-analyzer's availability probe ran `python3 --version` by subprocess
  — the one refused form — so under the shims it reported "Python is not
  available" on machines where it plainly is. It now probes
  sys.executable, the interpreter the analyzer itself runs under.
  Verified under the shim: probe returns True.
- The flag step-over in both script patterns handles long and
  value-taking flags (`python3 -W ignore harness.py`, `--verbose
  tool.py`), matching the shim's two-slot consumption.
- A bare `allow-legacy-python:` with no reason no longer exempts
  anything; the reason the docs demand is now enforced.
- `uv run {baseDir}/...` gets --no-project at the ten semgrep and
  culture-index call sites that round two missed, and the culture-index
  remediation strings now name that same runnable command instead of a
  --project mechanism nothing uses.
- pip gains cache/config; the pattern comment now says the subcommand
  list is deliberately a subset.
- Both filesystem scans skip .venv/node_modules-style directories, after
  a stray local .venv (left by this session's own uv probe, and invisible
  to CI) turned the path scan red.

Smaller review items: the uv-probe prose says "Phase 4 onward" rather
than a wrong phase range, run_fixtures' comment stops claiming PEP 723
headers its stdlib-only helpers do not have, the yarGen one-liners say
to run from the checkout, `uv tool install` sites note or export the
tool bin dir the way a fresh container needs, and sarif-parsing's table
column says Install / run and stops naming a file that does not exist.

Self-test 71 -> 74. Full scan: 0 findings over 773 files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 13:39:07 -04:00
Joop van de Pol 9ea55c5987 Fix constant-time analyzer backends and trim the skill (#220)
* Add per-language triage fixtures for the constant-time analyzer

One fixture per supported language, each pairing operations the analyzer
reports identically but that triage must separate: a division on a
private-key coefficient beside one on a public buffer length, and, where
the backend detects them, a weak RNG seeding a nonce beside the same call
jittering a retry delay.

Expected verdicts are deliberately absent from the fixture source. These
files double as evaluation input, and a verdict written next to the code
is a verdict the reviewer reads instead of deriving. A later commit adds
the manifest that records them.

Fixture shapes are dictated by what each toolchain emits: the Go helpers
are //go:noinline so each stays a distinct symbol, its main() takes input
from argv so the arithmetic is not constant-folded away, and the Swift
entry points use @_cdecl to keep symbol names readable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Configure ty and fix the typing weaknesses it reports

`ty check` reported 50 diagnostics, all pre-existing. Most were noise from
module resolution: the tests reach the modules via `sys.path.insert` and
import `analyzer` rather than `ct_analyzer.analyzer`, which ty cannot know
without being told. `tool.ty.environment extra-paths` tells it, dropping 32
unresolved-import reports and leaving the substantive ones visible.

The rest were real, if not yet bugs:

- Six compilers and several entry points annotated `list[str]`/`str` while
  defaulting to None. Now spelled `| None`.
- `get_compiler` was annotated to require a compiler name, but its body
  auto-detects from the language when the name is falsy, which is how every
  caller uses it.
- Each parser tracked functions as a bare dict, typing the values `str |
  int`, so the `functions[-1]["instructions"] += 1` counter present in all
  eight parsers read as string addition. A `ParsedFunction` TypedDict names
  the shape instead.

Three unresolved-import reports remain by construction: analyzer.py is
documented to run as a script as well as import as a package, so both the
relative and top-level spellings are needed and only one can resolve. Those
carry an inline ignore with the reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fix analyzer backends that reported PASSED on vulnerable code

Auditing each backend against real toolchain output found six of them
skipping unparseable lines and reporting success. Every one was covered by
tests that replay hand-written listings, which is why the drift went
unnoticed; the tests added here drive the actual toolchains.

Python, on 3.11 and later:
- The BINARY_OP oparg map read 12 as `//`, but 12 is `^`. Floor division on
  a secret was missed and every constant-time XOR was reported as a
  division. Keyed on the operator symbol dis prints, which cannot silently
  renumber.
- The instruction regex required a byte offset that 3.13 no longer prints,
  so all but the first instruction of each source line was skipped. On 3.13
  `key_coef // (2 * gamma2)` came back PASSED. Every column ahead of the
  opcode is now optional, and the source line carries forward.

PHP: `_parse_opcache_output` delegated to the VLD parser on the claim the
formats were "similar enough"; they share nothing structural. `analyze()`
also called the VLD parser unconditionally, so the function was dead either
way. VLD is an unbundled PECL build, so this is the path most users hit: a
file with intdiv, mt_rand and base64_encode reported zero findings. Adds a
real parser, plus intdiv/fdiv, which perform division through a call and so
emit no DIV opcode.

JavaScript and TypeScript: V8 prints `67 E> 0x… @ 14 : 3e 03 04  Div a0,
[4]`, not `offset : Mnemonic`, so every file parsed as zero instructions.
`--no-lazy` covers functions a module exports without calling. Bytecode
blocks are filtered to names the file declares, since V8 dumps node's
internals identically. Source positions are byte offsets, now converted to
lines except for transpiled TypeScript, where they index generated output.

Go: `go build` links the runtime in, so 23 of 24 findings came from the
allocator and garbage collector while the caller's code was silent. Objdump
headers name the source file, so the filter is exact. Go also writes arm64
in Plan 9 syntax, where a 32-bit divide is SDIVW — absent from the table, so
`int32` division, the shape of every polynomial coefficient divide, read as
clean.

Rust: compiled as a bin crate, so any library file failed E0601. Crypto
lives in libraries.

Swift: target triples were hardcoded to Apple platforms, so swiftc rejected
them on Linux and no Swift file could be analyzed there.

Java: the weak-RNG pattern matched only `new Random(`, missing the
fully-qualified form that needs no import.

All six source-level scanners treated comments as code, so `/** a / b */`
counted as a division; comment bodies are now blanked while preserving
offsets. Finally, the text report labelled every scripting-language error
`[WARN]` while the summary counted it as an error: analyzer.py runs as
`__main__` while script_analyzers imports it as `analyzer`, giving two
Severity enums, so identity comparison failed. Severity is compared by
value now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Record triage verdicts for every fixture and assert the pairs hold

expectations.json gives each fixture's cases a verdict and the reasoning
behind it: 53 cases over 13 languages, covering arithmetic everywhere,
conditional branches on the native backends, weak RNG wherever a backend
detects it, and PHP's encoding functions.

TestTriageMatrix asserts the mechanical half — that the analyzer still
reports both members of every true/false-positive pair, since the premise of
the skill's triage step is that the tool cannot tell them apart. Four of its
checks need no toolchain, so the matrix stays guarded on machines that
cannot compile most of these languages: every supported extension has a
fixture, every fixture pairs a true with a false positive, every line-based
locator resolves exactly once, and no fixture leaks its verdict into a
comment. The run itself fails rather than skipping when no fixture could be
exercised, and names the languages it skipped.

Two cases record measured behaviour rather than the intent: Rust claims no
branch true positive, because rustc emits no flagged conditional branch in
the tag comparison at O0 on arm64 and asserting one would encode a fixture
artefact; and Go's branch false positive is its stack-growth check, which
appears in every Go function and depends on nothing the caller passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Trim the constant-time-analysis skill and state its real coverage

SKILL.md goes from 219 to 165 lines by removing what was duplicated or
inert: a decision tree that restated the trigger bullets below it, five
near-identical invocation blocks, and JVM/.NET setup copied from
references/vm-compiled.md. It gains the frontmatter it was missing —
allowed-tools, which it never declared and so inherited everything, and an
explicit effort level.

The additions come from evaluating the skill rather than from review. Runs
at three effort levels all recommended fixing a secret-dependent divide by
handing the compiler a constant divisor; measuring that fix showed it still
emits a real divide on gcc riscv64 at every optimization level, on gcc arm64
and x86_64 at Os and Oz, and on clang arm64 at O0 and Oz. The sweep section
now says so, with the table.

Coverage is not uniform across backends, and a clean report means different
things depending on the language, so there is a table for that too. Triage
guidance now also covers the weak-RNG and encoding findings, where no
operand is secret and the question is what the result is used for — seeding
a nonce or jittering a retry delay.

"When NOT to Use" names the constant-time-testing skill and draws the line
between them: that one measures a running binary, this one reads compiler
output and never executes the code.

Both READMEs listed nine languages when thirteen are supported, and the
skill's file tree was missing four reference files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fix warning-level detectors that no source could ever match

Extending the triage matrix to the comparison, lookup and encoding families
found that several of those detectors were unreachable. The tables listed
them, so the coverage looked complete, but no input could trigger them.

- Ruby built every warning pattern as `\.<name>`, which is right for method
  calls like `.start_with?(` but turns the module-qualified keys into
  `\.base64\.encode64`. Five entries — both base64 functions, json.parse and
  both marshal functions — could not match any Ruby source.
- JavaScript did the same to its non-method entries, so `btoa(`, `atob(`,
  `decodeURIComponent(` and `JSON.parse(` never matched.
- The JS keyed-property mnemonics were V8 9.x spellings. Node 22 emits
  `GetKeyedProperty`, so secret-indexed array access — the whole point of
  that family — was undetectable. The named-property entry is removed rather
  than updated: a named access has a constant property name and cannot be
  indexed by a secret, so flagging it reports every `Math.trunc` call.
- Java, Kotlin and C# selected patterns with if/elif chains covering three or
  four keys and skipping the rest, leaving the base64 and convert entries
  unreachable. They now share `qualified_call_pattern`, which distinguishes
  the two shapes the keys use: `string.equals` is a method on any receiver,
  while `arrays.equals` and `base64.getencoder` are members of a named type
  and must match only the qualified form. Case-insensitivity is embedded in
  the pattern because the keys are lowercase while the source spells them
  `Base64.getEncoder` and `.Equals`.

Ruby's warning mnemonics also kept the dots from their keys, unlike every
other severity and backend, so they read `BASE64.ENCODE64`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Cover the comparison, lookup and encoding families in the triage matrix

The matrix covered two of the four families the analyzer implements. It now
covers all four, taking it from 53 cases to 99.

Each of the eight bytecode and scripting fixtures gains three pairs: an
early-exit comparison of a secret tag against the same comparison on a public
header, a table lookup indexed by a key byte against one indexed by a fixed
offset into a public header, and a base64 encode of a decrypted session key
against one of the public algorithm identifier. PHP already had the encoding
pair. Native fixtures are unchanged — their instruction tables carry no
comparison or load entries, and C's tag comparison is already covered as a
branch.

All three families are warning severity, so their cases run with
`include_warnings`. That is worth noting on its own: the invocation the skill
documents does not pass `--warnings`, so a reviewer following it sees none of
this — including early-exit comparison, which is the most common timing bug
in practice.

Every case was measured against the real toolchain before being written down.
Python's fixture avoids a `list[bytes]` annotation in a signature because the
subscript in the annotation is itself reported at module scope.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Refuse an architecture the compiler cannot target

Every compiler looked its architecture up with `.get()` and simply omitted the
target flag when the lookup missed, while `analyze_source` still labelled the
report with the architecture that was requested and applied that
architecture's instruction table. So `--arch riscv64` on Swift, whose Linux map
holds two entries, compiled for the host and reported `architecture: riscv64`
with no findings; `--arch mips` on gcc reported `passed: true` the same way.

"PASSED for riscv64" is what a reviewer writes down, so producing host output
under the requested label is worse than failing. All five compilers now refuse
and name what they do support.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Stop the Go symbol filter readmitting the runtime by basename

The filter compares the objdump block's source path against the file under
analysis, but when the realpath comparison failed it fell back to comparing
basenames — and Go's runtime ships map.go, slice.go, string.go, time.go and
select.go. Analyzing a user file named map.go therefore matched every
`TEXT runtime.…(SB) /usr/lib/go/src/runtime/map.go` block, reported
runtime.makeBucketArray among the findings, and parsed 14 functions instead of
5. That is the behaviour the filter exists to prevent, and the existing test
missed it only because its fixture is named triage_go.go.

objdump prints an absolute path, so the exact comparison is sufficient there;
the basename fallback now applies only when the printed path is relative.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Make comment blanking string-aware

Blanking comments before the source-level scans stopped doc comments counting
as code, but the regexes knew nothing about string literals, so a comment
marker inside one blanked live code:

- `const u = "http://example.com"; const k = a / b;` lost the division, and
  `String u = "http://x"; Random r = new Random();` reported clean — for Java,
  Kotlin and C# the source scan is the only detector for `new Random()`.
- Ruby's `puts "tag #{a / b}"` lost everything from the interpolation on.
- Worst, a lone `const marker = "/*";` plus any later `*/` anywhere in the file
  blanked every line in between, with no diagnostic.

String and template literals are now matched first and preserved. This is still
a regex rather than a lexer — unterminated literals, JavaScript regex literals,
Ruby heredocs and `%w[]` are not modelled — and the docstring says so; the
failure mode is a blanked or unblanked span, never a crash.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Test the empty-parse guards and close smaller robustness gaps

The test named for PHP's "nothing parsed" refusal asserted only that the parser
returned empty lists, which the pre-fix code did too, so deleting the raise in
`analyze()` left the suite green. It now asserts the RuntimeError, and the Go
equivalent — the symbol filter keeping nothing — is covered as well. Both were
confirmed to fail with their guard removed.

Four gaps that each degrade quietly:

- The OPcache offset group was `\d{4}` exactly, so an op_array past 9999
  opcodes stopped parsing mid-function. `functions` is non-empty by then, so
  the empty-parse guard cannot fire and the report reads as a partial pass.
- The V8 name filter treated "could not read the compiled file" as "filtering
  disabled", listing hundreds of node-internal functions with nothing to
  distinguish that from a genuinely noisy file. Unreadable input is now an
  explicit None with a note on stderr.
- `last_line_num` was not reset per code object, so the first instructions of a
  function inherited the previous function's line when dis omits one.
- The triage matrix printed its skip list to stdout, where CI summaries do not
  show it, so an image that lost a toolchain would keep passing while the claim
  that all backends are exercised quietly stopped being true. Skips are now
  emitted per language and appear in the count.

Two test-quality fixes: the matrix completeness check compared set sizes, so
renaming one language while dropping another stayed green — it compares sets
now, through an explicit display-name to `detect_language()` mapping. And
`_have` uses `shutil.which` rather than running `--version`, because javap
exits non-zero for it, which would have skipped Java and Kotlin everywhere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Pass --warnings in the documented invocation

The Quick Start ran the analyzer with no flags, which reports only
error-severity findings: division, modulo and weak RNG. Four families are
warning severity and stayed silent — secret-dependent branches, early-exit
comparison, secret-indexed table lookups, and variable-time encoding. A
reviewer following the skill exactly never saw the early-exit comparison of an
authentication tag, which is the most common timing bug in practice and what
Lucky Thirteen was.

`--warnings` is now in the documented command, the directory sweep and the flag
table, with the two consequences of turning it on stated alongside. Warnings do
not affect the PASSED line, so `Result: PASSED` beside `Warnings: 6` is normal
and is not a clean result. And comparison and lookup findings need their own
triage question: for a lookup it is the index that must be secret, not the
contents. Confirmed comparison findings have a per-language constant-time
primitive, so those are tabulated rather than left as "write it branch-free";
a secret-indexed lookup gets the honest answer that no drop-in exists.

Also names the plugin that owns constant-time-testing, since a reader cannot
assume it is installed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fix the fixture path and the sdist include list

Two defects that both make something unreachable:

The pointer to the triage fixtures was written as a bare relative path, while
every other path in SKILL.md is `{baseDir}`-prefixed. The skill runs against the
user's repository, so a model following it looked for
`ct_analyzer/tests/triage_samples/` inside the project being audited. The
validator resolves markdown links rather than inline code, so this passed 359
reference checks while being unusable at runtime.

The sdist include list named three globs covering C, Go and Rust, so a packaged
install shipped none of the Python, Ruby, PHP, JS, TS, Java, C#, Kotlin or Swift
samples, nor the triage fixtures and their manifest, and the tests that read
them could not run from an sdist. A built sdist now carries all 27.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Test that cross-compilation really targets the requested architecture

Nothing covered cross-compilation. The pre-existing cross-architecture test
asks for arm64, which is the host on an aarch64 machine, so it passes there
without ever crossing — and a silent fall back to the host is exactly the
failure `reject_arch` was added to prevent.

These assert instruction names unique to the target: x86 `DIVL`/`DIVQ`/`IDIVL`
and RISC-V `DIVU`/`DIVW`/`REMU` must appear, and arm64 `SDIV` must not. Dropping
clang's `--target` makes both fail, which a report-shape assertion would not
have caught. Go gets the same treatment for amd64, where GOARCH cross-builds
because CGO is disabled.

Only the paths that work are covered: on this host clang crosses to both
targets and Go to amd64, while gcc, rustc and Go/riscv64 cannot cross here at
all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Drive an explicit compiler with its own flags, and name it in the report

Cross-compiling with gcc was impossible through the analyzer. `--arch x86_64`
passed gcc's native ISA switch `-m64`, which the gcc on an arm64 host rejects,
and the escape hatch that should have worked — handing over the cross binary —
was broken: `get_compiler` treated every unrecognized `--compiler` value as
clang, so `--compiler x86_64-linux-gnu-gcc` was driven with `--target=` and
failed on "unrecognized command-line option".

An explicit compiler is now dispatched on which compiler it actually is: by
filename first, falling back to its `--version` banner, so `cc` is recognized
as whatever it points at. `--compiler x86_64-linux-gnu-gcc --arch x86_64` and
`--compiler riscv64-linux-gnu-gcc --arch riscv64` both work and emit that
target's division instructions.

Nothing is substituted on the caller's behalf. The analyzer could look for
`<triple>-gcc` and use it automatically, but a Debian cross gcc is frequently a
different major version than the host's, so its codegen is not the host gcc's;
silently swapping binaries would put one compiler's output under another's name
— the same mislabelling this commit removes. Instead, when gcc is asked for an
architecture it cannot build, the error names the binary to pass, and the report
records the binary that ran rather than the family, so `compiler:` is no longer
"gcc" when a cross build produced the assembly.

SKILL.md now states how each toolchain crosses, since "sweep more than one
--arch" was not actionable for gcc users, and advises comparing against the
toolchain that builds the product rather than a packaged cross build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Detect software division, and fix the armhf flags that hid it

Installing the remaining cross toolchains turned up two problems that are not
about missing dependencies.

The arm flags were self-contradictory: `-march=armv7-a -mfloat-abi=hard` is
rejected with "selected architecture lacks an FPU", because armv7-a alone
specifies no FPU. Adding `-mfpu=vfpv3-d16`, Debian's armhf baseline, makes the
build work.

With it building, arm reported no findings at all on a fixture with three
divisions. armv7-a has no hardware divider, so gcc emits `bl __aeabi_idiv`
rather than a division instruction, and the analyzer matches mnemonics: a
branch-with-link is not in any table, so the file read as PASSED. Software
division loops over its operands, making it more operand-dependent than the
instruction it replaces, so this was the worst kind of false negative. Calls to
the libgcc and Arm EABI division routines are now reported by call target, which
also covers __divti3 — used on x86_64 for __int128 division, which crypto code
does perform.

Verified against every cross target now installed: x86_64 DIVQ/IDIVL, i386
DIVL/IDIVL, arm AEABI_IDIV, riscv64 DIVU/DIVW/REMU, ppc64le DIVDU/DIVW, s390x
DLGR/DSGFR, arm64 SDIV/UDIV. The ppc64le and s390x instruction tables had never
been exercised before.

Also stops the new "pass a cross build" hint from telling the caller to do what
they just did: it is suppressed when the compiler already is a cross build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Cross-compile a freestanding probe, not a fixture that needs a cross libc

CI failed on `test_clang_targets_riscv64` with "bits/libc-header-start.h file
not found". The test compiled the C triage fixture, which includes <stdint.h>,
so cross-compiling it needs the target's C library headers as well as the
compiler. The x86_64 case passed only because the runner is x86_64, making that
target native; my dev box passed riscv64 only because installing
gcc-riscv64-linux-gnu had pulled in libc6-dev-riscv64-cross. So the test was
asserting an environment property, not the analyzer's behaviour.

Nothing about checking that a division instruction reaches the assembly needs a
libc. The cross tests now use a freestanding sample with no includes, verified
to compile for x86_64, riscv64 and s390x with `-nostdinc` — that is, with no
headers reachable at all — while still yielding that target's division
mnemonics.

This also corrects the claim in SKILL.md that clang needs nothing installed to
cross-compile: it needs no second compiler, but a source including libc headers
still needs the target's headers. When that is what failed, the error now names
the package that supplies them, which is the analyzer's own advice to sweep
`--arch` being made actionable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Scott Arciszewski <147527775+tob-scott-a@users.noreply.github.com>
Co-authored-by: kz-tob <kara.zaffarano@trailofbits.com>
2026-08-03 14:12:27 -04:00
Lixin2026 d5fe2e6a78 feat(codex): add UI metadata for skills (#175)
* feat(codex): add skill UI metadata

* Use official Trail of Bits logo

* fix: resolve code review findings for PR #175

Codex silently drops the icons as authored: its loader
(codex-rs/core-skills resolve_asset_path) requires icon paths
containing '..' to resolve under <plugin_root>/assets/, and the
repo-root .codex/assets location fails that containment check.
Verified empirically via codex app-server plugin/read: every
iconSmall/iconLarge came back null; only brand_color applied.

P1 fixed:
- Vendor trail-of-bits-mark.svg into plugins/<name>/assets/ for
  all 38 plugins with skills and point every openai.yaml at
  ../../assets/trail-of-bits-mark.svg (the supported plugin-level
  shared asset pattern). Icons now resolve for marketplace
  installs too, since nothing escapes the plugin root.
- Drop the .codex/ additions: .codex/skills/gh-cli/agents/
  openai.yaml resolved nowhere (.codex/skills is not a Codex
  discovery root) and PR #173 removes the whole .codex/ tree

P2 fixed:
- Patch-bump all 38 touched plugins in plugin.json and
  marketplace.json so installed clients pick up the metadata

Verified:
- Static check replicating Codex's resolution algorithm: all 73
  yaml files resolve under their plugin assets/ and exist
- Live codex app-server probe: 71/72 loadable skills report
  resolved iconSmall/iconLarge and brand_color #D83A34
  (claude-in-chrome-troubleshooting fails to load on main due to
  a pre-existing 64-char qualified-name limit, fixed by #173's
  rename; zeroize-audit's manifest mcpServers object is likewise
  a pre-existing Codex incompatibility fixed by #173)
- validate_codex_skills.py, validate_plugin_metadata.py, prek all
  pass

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(codex): use skill-local icon assets

---------

Co-authored-by: Dan Guido <dan@trailofbits.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:28:41 -04:00
Jonathan Hefner debfb29c8e Fix allowed-tools to use spec-compliant space-delimited strings (#139)
* Fix `allowed-tools` to use spec-compliant space-delimited strings

Per the agentskills.io specification, `allowed-tools` must be a single
string of space-delimited patterns, not a YAML list. Converted all 23
SKILL.md files from the `- Item` list format to the correct
`"Item1 Item2"` string format. Also updated the frontmatter examples in
CLAUDE.md and the workflow-skill-design skill template to match.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix remaining allowed-tools format in firebase-apk-scanner and workflow-skill-design docs

- Convert firebase-apk-scanner from comma-separated to space-delimited
- Update anti-patterns.md and tool-assignment-guide.md examples from YAML lists to space-delimited strings
- Remove unnecessary quotes from SKILL.md template placeholder

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Cover commands, new SKILL.md files, and fix template placeholder

Extends the previous spec-compliance fixes:

* Convert command frontmatter (commands/*.md) — per Claude Code
  docs, command files use the same frontmatter as skills, so the
  same space-delimited rule applies.
* Convert three SKILL.md files added since the original PR:
  mutation-testing, trailmark-structural, trailmark-summary.
* Fix the placeholder in the workflow-skill-design template.
  The previous "[minimum tools needed, space-delimited]" was YAML
  flow-sequence syntax, which parses as a list — the opposite of
  what the placeholder claims. Replaced with a concrete-looking
  space-delimited example plus a comment.

Zeroize-audit agent files still use `allowed-tools:` in YAML list
form. They are intentionally excluded: per the project's own docs
(workflow-skill-design references), agents declare tools with
`tools:` (not `allowed-tools:`). Fixing those requires changing
the field name as well as the format and is out of scope for this
PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* zeroize-audit agents: switch allowed-tools to tools

Subagents declare their tool allowlist via `tools:` (comma-separated),
not `allowed-tools:` — see Claude Code's subagent docs and this
repo's own designing-workflow-skills/SKILL.md:47:

> Skills use `allowed-tools:` in frontmatter. Agents use `tools:`
> in frontmatter.

Before this change, the zeroize-audit agents declared their tool list
under `allowed-tools:`, which Claude Code does not read for subagents.
The field was effectively a no-op; the spawned agents had no tool
restriction enforced.

Renames the field on all 11 agents to `tools:` and reformats the YAML
list as comma-separated to match the documented format and existing
agents elsewhere in the repo (e.g. function-analyzer.md,
spec-compliance-checker.md). Tool sets are unchanged.

Behavior change: tools now actually constrain what each spawned agent
can call. The lists are the ones the original author intended.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* skill-improver: convert command allowed-tools to space-delimited

The two command files in plugins/skill-improver/commands/ still used
the JSON flow-array format (`allowed-tools: ["..."]`), which the rest
of this PR converted everywhere else. Convert them to the spec-compliant
space-delimited string form for consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Dan Guido <dan@trailofbits.com>
2026-04-28 19:50:30 -04:00
newklei 52314112b9 fix(ct_analyzer): escape XML special chars in _compile_csharp() (#106)
* fix(ct_analyzer): escape XML special chars in _compile_csharp()

source_path and output_dir were embedded into the .csproj f-string
without escaping. On Linux, filenames may contain XML-special chars
(", <, >, &), allowing a crafted filename to inject arbitrary MSBuild
XML (CWE-91 -> CWE-78).

Fix: wrap both values with xml.sax.saxutils.escape() before
interpolation. The extra {chr(34): '&quot;'} arg covers double-quotes
inside the attribute value (chr(34) avoids an f-string quote conflict
on Python < 3.12).

* Update script_analyzers.py

---------

Co-authored-by: Scott Arciszewski <147527775+tob-scott-a@users.noreply.github.com>
2026-02-26 09:07:19 -05:00
Dan Guido 45e2ed25bc Fix Windows compatibility: remove colons from command filenames (#52)
Rename 10 command files that contained `:` in their filenames, which is
invalid on Windows filesystems (reserved for drive letters).

The `trailofbits:` namespace is preserved in the frontmatter `name` field,
so slash commands like `/trailofbits:audit-context` continue to work.

Fixes #51

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:07:05 -05:00
Dan Guido a6ed466329 Add trailofbits: prefixed slash commands to 9 plugins (#49)
Add slash commands that wrap existing skills for easier invocation:
- /trailofbits:scan-apk - Firebase APK security scanner
- /trailofbits:burp-search - Burp Suite project file search
- /trailofbits:entry-points - Smart contract entry point analyzer
- /trailofbits:variants - Vulnerability variant analysis
- /trailofbits:semgrep-rule - Semgrep rule creator
- /trailofbits:diff-review - Differential security review
- /trailofbits:ct-check - Constant-time analysis
- /trailofbits:spec-compliance - Spec-to-code compliance checker
- /trailofbits:audit-context - Audit context builder

Also rename existing fix-review command to trailofbits:fix-review
for consistency with the naming convention.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 00:11:55 -05:00
Dan Guido fd367ad81b ci: add lint enforcement with ruff, shellcheck, and shfmt (#10)
* ci: add lint enforcement with ruff, shellcheck, and shfmt

Add pre-commit hooks (prek) and GitHub Actions CI to enforce Python and
shell linting across the repository.

- Add root pyproject.toml with ruff configuration (line-length=100, py311)
- Add .pre-commit-config.yaml with ruff, shellcheck, shfmt, and standard hooks
- Add .github/workflows/lint.yml with SHA-pinned actions
- Update .github/dependabot.yml with root pip ecosystem entry
- Fix existing lint violations:
  - Auto-fix imports and formatting with ruff
  - Fix duplicate dict key in ct_analyzer (contentequals -> arrays.contentequals)
  - Add noqa comment for required sys.path manipulation in extract_pdf.py
  - Format shell scripts with shfmt (case statement indentation)
- Add per-file-ignores for ct_analyzer's long opcode tables and style patterns

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use 2-space indentation for shell scripts

Change shfmt configuration from 4-space to 2-space indentation
to match CLAUDE.md standards.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 15:13:46 -05:00
Dan Guido 695119c312 Initial release of Trail of Bits Skills Marketplace
16 plugins for security analysis, smart contract auditing, and verification:

Smart Contract Security:
- building-secure-contracts
- entry-point-analyzer

Code Auditing:
- audit-context-building
- burpsuite-project-parser
- differential-review
- semgrep-rule-creator
- sharp-edges
- testing-handbook-skills
- variant-analysis

Verification:
- constant-time-analysis
- property-based-testing
- spec-to-code-compliance

Audit Lifecycle:
- fix-review

Reverse Engineering:
- dwarf-expert

Development:
- ask-questions-if-underspecified

Team Management:
- culture-index

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 15:24:03 -05:00