Security analysis
ovecc ships two complementary security surfaces, both deterministic and offline:
ovecc security— findings in your code: hardcoded secrets, insecure patterns, weak crypto, permissive CORS, and tainted source→sink flows.ovecc audit— known vulnerabilities in your dependencies, checked against the OSV database.
Neither uses an LLM. Everything comes with file:line evidence.
The code security sweep
$ ovecc security
Security findings: 13 (scanned the indexed repository)
secrets 1, insecure 2, weak-crypto 1, cors 1, tainted-flows 8
[Critical] Hardcoded secret: Stripe secret key
Evidence: src/services/billing.ts:5 (Stripe secret key)
[Critical] Potential tainted flow: POST /admin/eval -> eval (eval)
Evidence: src/routes/api.ts:55 (sink: eval)
Evidence: src/routes/api.ts:54 (source: POST /admin/eval)
[High] OS command execution: execSync
Evidence: src/routes/api.ts:60 (execSync)
[High] Potential tainted flow: POST /billing/charge -> invoices (writes)
Evidence: src/services/billing.ts:18 (sink: writes)
Evidence: src/routes/api.ts:29 (source: POST /billing/charge)
[Medium] Permissive CORS configuration: Access-Control-Allow-Origin: *
Evidence: src/routes/api.ts:12 (Access-Control-Allow-Origin: *)
[Medium] Weak hash algorithm: MD5
Evidence: src/utils/auth.ts:4 (MD5)
...
What the detectors cover
| Category | Rule | Detected patterns |
|---|---|---|
| Secrets | security/secret (critical) | Provider-pattern credentials (e.g. AWS access keys, Stripe live keys) and high-entropy values assigned to secret-named bindings. Well-known documentation placeholder keys are deliberately not flagged. |
| Dynamic code | security/eval (high) | eval(...), new Function(...) |
| Command execution | security/command-exec (high) | exec / execSync / spawn (and friends) called on a child_process-style module object; os.system / subprocess.* in Python; exec.Command in Go; process::Command in Rust |
| Weak crypto | security/weak-hash (medium) | MD5, SHA-1 (e.g. createHash("md5"), hashlib.sha1) |
| Permissive CORS | security/cors (medium) | origin: "*" middleware config and the raw setHeader("Access-Control-Allow-Origin", "*") form |
| Tainted flows | taint/* | see below |
Findings located in test code are down-ranked to Low rather than hidden.
Taint tracking: route → sink
At index time, ovecc connects extracted HTTP routes to dangerous sinks
(database reads/writes, eval, command execution) through the resolved call graph —
including inline arrow handlers, which get a synthetic symbol so the route stays
connected:
[Critical] Potential tainted flow: GET /admin/ping -> command (command)
Evidence: src/routes/api.ts:60 (sink: command)
Evidence: src/routes/api.ts:59 (source: GET /admin/ping)
Severity depends on the sink:
| Rule | Sink | Severity |
|---|---|---|
taint/eval | dynamic code execution | critical |
taint/command | OS command execution | critical |
taint/writes | database write (injection candidate) | high |
taint/reads | database read | medium |
:::note Honest over-approximation
Taint flows are reachability-based: "user-controlled input from this route may
reach this sink via this call path." A parameterized query on a reachable path will
still be reported as a medium taint/reads — the finding's description says
Reachability over-approximation — requires review explicitly. Treat critical/high
flows as action items and medium read-flows as a review checklist.
:::
The JSON output includes the full propagation path:
{
"kind": "tainted_flow",
"severity": "critical",
"rule_name": "taint/command",
"title": "Potential tainted flow: GET /admin/ping -> command (command)",
"description": "User-controlled input from GET /admin/ping may reach OS command execution via GET /admin/ping -> <GET /admin/ping handler>. Reachability over-approximation — requires review.",
"evidence": [
{ "file_path": "src/routes/api.ts", "line": 61, "symbol": "<GET /admin/ping handler>", "detail": "sink: command" },
{ "file_path": "src/routes/api.ts", "line": 60, "detail": "source: GET /admin/ping" }
]
}
Dependency auditing (OSV)
audit checks declared dependencies against the local
OSV advisory cache in .ovecc/osv/. It is offline by default and honest when the
cache is empty:
$ ovecc audit
Dependency audit (OSV): scanned 3 package(s) against 0 advisor(ies)
Vulnerabilities: 0
(no OSV database in .ovecc/osv/ — sync advisories to enable matching)
--fetch downloads the advisories for exactly the packages you use — the only
ovecc operation that ever touches the network, and only with this flag:
$ ovecc audit --fetch
Fetched 8 new advisory(ies) for 3 package(s).
Dependency audit (OSV): scanned 3 package(s) against 8 advisor(ies)
Vulnerabilities: 8
[High] Vulnerable dependency: lodash@4.17.15 (GHSA-35jh-r3h4-6jhm)
[High] Vulnerable dependency: express@4.18.2 (GHSA-rv95-896h-c2vc)
...
Severity follows the advisory's own label; unlabeled advisories default to High.
npm package-lock.json is read today; other lockfile formats are planned.
Advisories are tracked as their own metric (dependency_advisories) — separate from
security_findings — so fetching fresh advisories never shows up as a code-quality
regression in drift.
Security in CI
# fail the build on any new high-severity code finding
ovecc security --fail-on high
# GitHub code scanning (SARIF upload)
ovecc violations --format sarif > ovecc.sarif
# fail on vulnerable dependencies
ovecc audit --fetch --fail-on high
See CI integration for full pipelines, and note that a failing
--fail-on still prints the complete report (exit code 1 is a signal, not a
crash — exit codes).
Suppressing a finding
False positive, or accepted risk? Silence one specific finding inline:
const digest = createHash("md5").update(cacheKey).digest("hex"); // ovecc-ignore
// ovecc-ignore-next-line and Python's # ovecc-ignore also work; the finding is
dropped at index time. ovecc even reports suppressions that no longer suppress
anything (stale-suppression), so dead ignores don't silently swallow the next real
finding on that line. See governance.