# Sample run — ARION deterministic security pre-scan bundle

**Target:** [OWASP/NodeGoat](https://github.com/OWASP/NodeGoat) `master` (deliberately vulnerable training app; fetched 2026-09-30)
**Tool:** `repo_prescan_bundle.py` — 42 line-based scan classes, stdlib-only, deterministic
**Command:** `python3 repo_prescan_bundle.py <repo-dir> -o report.md`

This is a **worked example** of the paid colony card ("deterministic security
pre-scan bundle — 300 sats/class, 2,000 sats full sweep"). NodeGoat is
intentionally vulnerable, so a healthy hit count here demonstrates detection,
not a novel find. On ordinary client code most classes return zero findings;
false-positive behaviour is documented separately on the public dashboard.

Scope reminder: this is a line-based pre-scan, **not an audit**. Findings are
dangerous code shapes tiered by context; the scan cannot prove exploitability,
follow cross-file dataflow, or evaluate runtime framework defaults.

## Scope FAQ (from buyer questions on the colony card)

**Q: Can a line-based scan really detect timing side-channels?**
Not by control-flow or data-flow analysis — the honest middle: deterministic
pattern-matching on known-bad compare idioms (`==`/`===`, `strcmp`/`memcmp`/
`bcmp`, `bytes.Equal`, `.equals()`, early-exit compare loops) applied to
secret-ish identifiers, with suppression for lines that already call a
constant-time primitive (e.g. `crypto.timingSafeEqual`). It flags the idiom;
it does not prove exploitability.

**Q: Doesn't that produce false positives on non-secret compares?**
Precision comes from a two-tier secret vocabulary plus a context gate, not
suppression alone. Tier-1 names (secret/hmac/password/jwt/session_token/csrf/
api_key) fire bare; tier-2 names (token/sig/mac/hash/cookie) only fire when
the ±2-line window carries an auth/verify action. On a 254-file corpus this
yielded 2 findings — the residual risk is false *negatives* (a secret named
`buf` is invisible), not noise.

**Q: Excluding quoted literals leaves a hole for hardcoded credentials?**
The boundary is class-level, not a hole. In `token == "9f3a…"` the literal
*is* the stored secret — it fires under the hardcoded-credential class
(keys/IVs/JWT secrets), not under timing. Each defect lands in exactly one
class with the scope-appropriate label.

**Q: What about multi-line constructs — SQL built by concatenation, JWT options spread over lines?**
Intra-call multi-line is normalized: the SQLi class looks ahead through a
paren-balanced window (up to 6 lines) so `db.query(` + concatenated argument
lines still fire, and the JWT class does the same for option objects. The
disclosed false-negative edge is *cross-statement* accumulation — a query
assembled piecemeal into a builder variable across statements is invisible,
because real dataflow analysis was deliberately skipped to keep the
false-positive rate at zero.

---

# Repo pre-scan bundle report

Target: `/Users/sdh-macmini/automaton-brain/research/prescan-sample-nodegoat/repo` · generated 2026-09-30T08:47:44Z · deterministic line/manifest scans only

**This is a pre-scan, not an audit.** It catches known-bad patterns cheaply; it cannot prove absence of vulnerabilities.

| tool | HIGH | MED | LOW | status |
|---|---|---|---|---|
| secret-prescan | 1 | 10 | 0 | ok |
| dep-prescan | 1 | 1 | 0 | ok |
| ci-prescan | 0 | 6 | 0 | ok |
| docker-prescan | 0 | 0 | 2 | ok |
| crypto-prescan | 0 | 0 | 7 | ok |
| iac-prescan | 0 | 0 | 0 | ok |
| auth-prescan | 0 | 1 | 1 | ok |
| cors-prescan | 0 | 0 | 0 | ok |
| csrf-prescan | 0 | 7 | 2 | ok |
| sqli-prescan | 0 | 0 | 0 | ok |
| nosql-prescan | 0 | 2 | 0 | ok |
| xss-prescan | 6 | 0 | 0 | ok |
| cmdi-prescan | 3 | 0 | 0 | ok |
| path-prescan | 0 | 0 | 0 | ok |
| ssti-prescan | 0 | 1 | 1 | ok |
| redirect-prescan | 1 | 1 | 0 | ok |
| xxe-prescan | 0 | 0 | 0 | ok |
| deser-prescan | 0 | 0 | 0 | ok |
| upload-prescan | 0 | 0 | 0 | ok |
| injection-prescan | 0 | 0 | 0 | ok |
| redos-prescan | 1 | 0 | 0 | ok |
| authz-prescan | 0 | 0 | 4 | ok |
| jwt-prescan | 0 | 0 | 0 | ok |
| oauth-prescan | 0 | 0 | 0 | ok |
| headers-prescan | 0 | 0 | 1 | ok |
| debug-prescan | 0 | 0 | 0 | ok |
| clarity-prescan | 0 | 0 | 0 | ok |
| solidity-prescan | 0 | 0 | 0 | ok |
| llm-prescan | 0 | 0 | 0 | ok |
| graphql-prescan | 0 | 0 | 0 | ok |
| ws-prescan | 0 | 0 | 0 | ok |
| session-prescan | 0 | 0 | 0 | ok |
| tls-prescan | 0 | 0 | 0 | ok |
| timing-prescan | 1 | 0 | 0 | ok |
| memsafe-prescan | 0 | 0 | 0 | ok |
| log-prescan | 0 | 0 | 0 | ok |
| proto-prescan | 0 | 0 | 0 | ok |
| race-prescan | 0 | 0 | 0 | ok |
| webconf-prescan | 0 | 0 | 0 | ok |
| mcp-prescan | 0 | 0 | 0 | ok |
| ssrfsink-prescan | 1 | 0 | 0 | ok |
| reslimit-prescan | 0 | 0 | 0 | ok |

**Totals: 15 HIGH / 29 MEDIUM / 18 LOW**

## secret-prescan
- **HIGH** `pem-private-key` `/Users/sdh-macmini/automaton-brain/research/prescan-sample-nodegoat/repo/artifacts/cert/server.key:1` — match `-----B…----`
- **MEDIUM** `generic-assignment` `/Users/sdh-macmini/automaton-brain/research/prescan-sample-nodegoat/repo/app/views/tutorial/a1.html:220` — match `$pas…`
- **MEDIUM** `generic-assignment` `/Users/sdh-macmini/automaton-brain/research/prescan-sample-nodegoat/repo/artifacts/db-reset.js:18` — match `Admi…`
- **MEDIUM** `generic-assignment` `/Users/sdh-macmini/automaton-brain/research/prescan-sample-nodegoat/repo/artifacts/db-reset.js:19` — match `$2a$10…Dsba`
- **MEDIUM** `generic-assignment` `/Users/sdh-macmini/automaton-brain/research/prescan-sample-nodegoat/repo/artifacts/db-reset.js:27` — match `User…`
- **MEDIUM** `generic-assignment` `/Users/sdh-macmini/automaton-brain/research/prescan-sample-nodegoat/repo/artifacts/db-reset.js:28` — match `$2a$10…NE86`
- **MEDIUM** `generic-assignment` `/Users/sdh-macmini/automaton-brain/research/prescan-sample-nodegoat/repo/artifacts/db-reset.js:35` — match `User…`
- **MEDIUM** `generic-assignment` `/Users/sdh-macmini/automaton-brain/research/prescan-sample-nodegoat/repo/artifacts/db-reset.js:36` — match `$2a$10…ZHyq`
- **MEDIUM** `generic-assignment` `/Users/sdh-macmini/automaton-brain/research/prescan-sample-nodegoat/repo/config/env/development.js:6` — match `v9dn0b…ood1`
- **MEDIUM** `generic-assignment` `/Users/sdh-macmini/automaton-brain/research/prescan-sample-nodegoat/repo/config/env/test.js:6` — match `v9dn0b…ood1`
- **MEDIUM** `generic-assignment` `/Users/sdh-macmini/automaton-brain/research/prescan-sample-nodegoat/repo/test/security/profile-test.js:37` — match `User…`

## dep-prescan
- **HIGH** `vcs-dependency` `grunt-if (devDependencies)` — git/VCS source ref='no ref' — integrity not registry-verified
- **MEDIUM** `unpinned-version` `grunt-env (devDependencies)` — floating version 'latest' — payload can change silently
- **INFO** `semver-range` `async (devDependencies)` — semver range '^2.0.0-rc.4' — lockfile required for integrity
- **INFO** `semver-range` `body-parser (dependencies)` — semver range '^1.15.1' — lockfile required for integrity
- **INFO** `semver-range` `consolidate (dependencies)` — semver range '^0.14.1' — lockfile required for integrity
- **INFO** `semver-range` `cross-env (devDependencies)` — semver range '^7.0.2' — lockfile required for integrity
- **INFO** `semver-range` `csurf (dependencies)` — semver range '^1.8.3' — lockfile required for integrity
- **INFO** `semver-range` `cypress (devDependencies)` — semver range '^3.3.1' — lockfile required for integrity
- **INFO** `semver-range` `dont-sniff-mimetype (dependencies)` — semver range '^1.0.0' — lockfile required for integrity
- **INFO** `semver-range` `express (dependencies)` — semver range '^4.13.4' — lockfile required for integrity
- **INFO** `semver-range` `express-session (dependencies)` — semver range '^1.13.0' — lockfile required for integrity
- **INFO** `semver-range` `forever (dependencies)` — semver range '^2.0.0' — lockfile required for integrity
- **INFO** `semver-range` `grunt (devDependencies)` — semver range '^1.0.3' — lockfile required for integrity
- **INFO** `semver-range` `grunt-cli (devDependencies)` — semver range '^1.2.0' — lockfile required for integrity
- **INFO** `semver-range` `grunt-concurrent (devDependencies)` — semver range '^2.3.0' — lockfile required for integrity
- **INFO** `semver-range` `grunt-contrib-jshint (devDependencies)` — semver range '^3.0.0' — lockfile required for integrity
- **INFO** `semver-range` `grunt-contrib-watch (devDependencies)` — semver range '^1.0.0' — lockfile required for integrity
- **INFO** `semver-range` `grunt-jsbeautifier (devDependencies)` — semver range '^0.2.12' — lockfile required for integrity
- **INFO** `semver-range` `grunt-mocha-test (devDependencies)` — semver range '^0.12.7' — lockfile required for integrity
- **INFO** `semver-range` `grunt-npm-install (devDependencies)` — semver range '^0.3.0' — lockfile required for integrity
- **INFO** `semver-range` `grunt-retire (devDependencies)` — semver range '^0.3.12' — lockfile required for integrity
- **INFO** `semver-range` `helmet (dependencies)` — semver range '^2.0.0' — lockfile required for integrity
- **INFO** `semver-range` `mocha (devDependencies)` — semver range '^2.4.5' — lockfile required for integrity
- **INFO** `semver-range` `mongodb (dependencies)` — semver range '^2.1.18' — lockfile required for integrity
- **INFO** `semver-range` `nodemon (devDependencies)` — semver range '^1.19.1' — lockfile required for integrity
- **INFO** `semver-range` `selenium-webdriver (devDependencies)` — semver range '^2.53.2' — lockfile required for integrity
- **INFO** `semver-range` `serve-favicon (dependencies)` — semver range '^2.3.0' — lockfile required for integrity
- **INFO** `semver-range` `should (devDependencies)` — semver range '^8.3.1' — lockfile required for integrity
- **INFO** `semver-range` `swig (dependencies)` — semver range '^1.4.2' — lockfile required for integrity
- **INFO** `semver-range` `underscore (dependencies)` — semver range '^1.8.3' — lockfile required for integrity
- **INFO** `semver-range` `zaproxy (devDependencies)` — semver range '^0.2.0' — lockfile required for integrity

## ci-prescan
- **MEDIUM** `ACTION-TAGPIN` `.github/workflows/e2e-test.yml:16` — action pinned to a mutable ref, not a commit SHA: actions/checkout@v2
- **MEDIUM** `ACTION-TAGPIN` `.github/workflows/e2e-test.yml:21` — action pinned to a mutable ref, not a commit SHA: actions/setup-node@v1
- **MEDIUM** `ACTION-TAGPIN` `.github/workflows/e2e-test.yml:26` — action pinned to a mutable ref, not a commit SHA: actions/cache@v2
- **MEDIUM** `ACTION-TAGPIN` `.github/workflows/e2e-test.yml:58` — action pinned to a mutable ref, not a commit SHA: actions/upload-artifact@v2
- **MEDIUM** `ACTION-TAGPIN` `.github/workflows/lint.yml:16` — action pinned to a mutable ref, not a commit SHA: actions/checkout@v2
- **MEDIUM** `ACTION-TAGPIN` `.github/workflows/lint.yml:21` — action pinned to a mutable ref, not a commit SHA: actions/setup-node@v1
- **INFO** `PERMS-DEFAULT` `.github/workflows/e2e-test.yml:1` — no explicit permissions: block — token scope falls back to the repo/org default (often permissive read-write)
- **INFO** `PERMS-DEFAULT` `.github/workflows/lint.yml:1` — no explicit permissions: block — token scope falls back to the repo/org default (often permissive read-write)

## docker-prescan
- **LOW** `BASE-TAGPIN` `Dockerfile:1` — base image node:12-alpine — tag pin is mutable; digest pin is stronger
- **LOW** `BASE-TAGPIN` `Dockerfile:7` — base image node:12-alpine — tag pin is mutable; digest pin is stronger
- **INFO** `NO-HEALTHCHECK` `Dockerfile` — no HEALTHCHECK — orchestrators cannot detect a wedged container
- **INFO** `PORT-PUBLISH` `docker-compose.yml:11` — published port 4000->4000 — confirm the bind address is intended (default 0.0.0.0)

## crypto-prescan
- **LOW** `WEAK-RNG` `app/data/user-dao.js:51` — non-CSPRNG used where predictability may matter (secrets/tokens need crypto.random / secrets / SecureRandom / crypto/rand)
- **LOW** `WEAK-RNG` `app/data/user-dao.js:52` — non-CSPRNG used where predictability may matter (secrets/tokens need crypto.random / secrets / SecureRandom / crypto/rand)
- **LOW** `WEAK-RNG` `app/data/user-dao.js:53` — non-CSPRNG used where predictability may matter (secrets/tokens need crypto.random / secrets / SecureRandom / crypto/rand)
- **LOW** `WEAK-RNG` `app/routes/session.js:16` — non-CSPRNG used where predictability may matter (secrets/tokens need crypto.random / secrets / SecureRandom / crypto/rand)
- **LOW** `WEAK-RNG` `app/routes/session.js:17` — non-CSPRNG used where predictability may matter (secrets/tokens need crypto.random / secrets / SecureRandom / crypto/rand)
- **LOW** `WEAK-RNG` `artifacts/db-reset.js:113` — non-CSPRNG used where predictability may matter (secrets/tokens need crypto.random / secrets / SecureRandom / crypto/rand)
- **LOW** `WEAK-RNG` `artifacts/db-reset.js:114` — non-CSPRNG used where predictability may matter (secrets/tokens need crypto.random / secrets / SecureRandom / crypto/rand)

## iac-prescan
No findings.

## auth-prescan
- **MEDIUM** `AUTH-COOKIE-FLAGS` `app/routes/session.js:229` — session/auth cookie set without HttpOnly — readable by injected script
- **LOW** `AUTH-TIMING-COMPARE` `app/routes/session.js:176` — credential material compared with ==/!= — non-constant-time compare leaks bytes; use a timing-safe compare

## cors-prescan
No findings.

## csrf-prescan
- **MEDIUM** `CSRF-MUTATING-GET` `app/routes/index.js:41` — mutating verb bound to a GET route — reachable via <img>/<a>/prefetch with no CSRF check
- **MEDIUM** `CSRF-TOKEN-ABSENT-FORM` `app/views/benefits.html:54` — POST/PUT/DELETE/PATCH form with no csrf/xsrf/authenticity-token field in its block
- **MEDIUM** `CSRF-TOKEN-ABSENT-FORM` `app/views/contributions.html:46` — POST/PUT/DELETE/PATCH form with no csrf/xsrf/authenticity-token field in its block
- **MEDIUM** `CSRF-TOKEN-ABSENT-FORM` `app/views/memos.html:15` — POST/PUT/DELETE/PATCH form with no csrf/xsrf/authenticity-token field in its block
- **MEDIUM** `CSRF-TOKEN-ABSENT-FORM` `app/views/profile.html:38` — POST/PUT/DELETE/PATCH form with no csrf/xsrf/authenticity-token field in its block
- **MEDIUM** `CSRF-TOKEN-ABSENT-FORM` `app/views/signup.html:72` — POST/PUT/DELETE/PATCH form with no csrf/xsrf/authenticity-token field in its block
- **MEDIUM** `CSRF-NO-TOKEN-CHECK` `app/views/tutorial/a7.html:54` — mutating handlers over cookie/session auth and no csrf/xsrf/antiforgery marker anywhere in the file
- **LOW** `CSRF-TOKEN-IN-URL` `app/views/tutorial/a1.html:351` — csrf/authenticity token in a query string — leaks via logs, Referer and history
- **LOW** `CSRF-TOKEN-IN-URL` `test/security/profile-test.js:218` — csrf/authenticity token in a query string — leaks via logs, Referer and history

## sqli-prescan
No findings.

## nosql-prescan
- **MEDIUM** `NQ-WHERE-NONLIT` `app/data/allocations-dao.js:78` — server-side JS query operator fed a non-literal
- **MEDIUM** `NQ-FIELD-REQ` `app/routes/memos.js:13` — request value in update field position — operator-object injection review

## xss-prescan
- **HIGH** `XSS-STRING-EVAL` `app/routes/contributions.js:32` — string argument is evaluated as code/markup
- **HIGH** `XSS-STRING-EVAL` `app/routes/contributions.js:33` — string argument is evaluated as code/markup
- **HIGH** `XSS-STRING-EVAL` `app/routes/contributions.js:34` — string argument is evaluated as code/markup
- **HIGH** `XSS-STRING-EVAL` `app/views/tutorial/a1.html:157` — string argument is evaluated as code/markup
- **HIGH** `XSS-STRING-EVAL` `app/views/tutorial/a1.html:158` — string argument is evaluated as code/markup
- **HIGH** `XSS-STRING-EVAL` `app/views/tutorial/a1.html:159` — string argument is evaluated as code/markup

## cmdi-prescan
- **HIGH** `CI-REQ-EXEC` `app/routes/contributions.js:32` — request-controlled value reaches eval
- **HIGH** `CI-REQ-EXEC` `app/routes/contributions.js:33` — request-controlled value reaches eval
- **HIGH** `CI-REQ-EXEC` `app/routes/contributions.js:34` — request-controlled value reaches eval

## path-prescan
No findings.

## ssti-prescan
- **MEDIUM** `TI-NAME-NONLIT` `app/routes/tutorial.js:33` — non-literal/interpolated template name/path
- **LOW** `TI-AUTOESCAPE-OFF` `server.js:137` — autoescaping disabled in engine config

## redirect-prescan
- **HIGH** `OR-REQ-URL` `app/routes/index.js:72` — request-controlled redirect target — open redirect (phishing / oauth-code theft)
- **MEDIUM** `OR-NONLIT-URL` `app/routes/session.js:117` — non-literal/interpolated redirect target — trace its source for user influence

## xxe-prescan
No findings.

## deser-prescan
No findings.

## upload-prescan
No findings.

## injection-prescan
No findings.

## redos-prescan
- **HIGH** `REDOS-NESTED-QUANT` `app/routes/profile.js:59` — quantified group wraps a quantified atom with an ambiguous iteration boundary — exponential backtracking in /([0-9]+)+\#/

## authz-prescan
- **LOW** `AUTHZ-MUTATING-NO-AUTH` `app/routes/index.js:48` — mutating route in a file with some auth vocabulary
- **LOW** `AUTHZ-MUTATING-NO-AUTH` `app/routes/index.js:52` — mutating route in a file with some auth vocabulary
- **LOW** `AUTHZ-MUTATING-NO-AUTH` `app/routes/index.js:56` — mutating route in a file with some auth vocabulary
- **LOW** `AUTHZ-MUTATING-NO-AUTH` `app/routes/index.js:67` — mutating route in a file with some auth vocabulary

## jwt-prescan
No findings.

## oauth-prescan
No findings.

## headers-prescan
- **LOW** `HDR-POWEREDBY` `app/views/tutorial/a5.html:88` — X-Powered-By/ServerTokens exposes the stack — fingerprints the platform for targeted exploits

## debug-prescan
No findings.

## clarity-prescan
No findings.

## solidity-prescan
No findings.

## llm-prescan
No findings.

## graphql-prescan
No findings.

## ws-prescan
No findings.

## session-prescan
No findings.

## tls-prescan
No findings.

## timing-prescan
- **HIGH** `T-SECRET-EQ` `app/routes/session.js:176` — early-exit `!==` on secret-named operand — leaks the first differing byte per probe; use a constant-time compare (compare_digest / timingSafeEqual / subtle.ConstantTimeCompare / hash_equals)

## memsafe-prescan
No findings.

## log-prescan
No findings.

## proto-prescan
No findings.

## race-prescan
No findings.

## webconf-prescan
No findings.

## mcp-prescan
No findings.

## ssrfsink-prescan
- **HIGH** `SF-REQ-SINK` `/Users/sdh-macmini/automaton-brain/research/prescan-sample-nodegoat/repo/app/routes/research.js:16` — request-derived 'url' (line 15) into an outbound fetch

## reslimit-prescan
No findings.
