sentinelayer-cli 0.6.2 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1009 -996
- package/bin/create-sentinelayer.js +5 -5
- package/bin/sentinelayer-cli.js +4 -4
- package/bin/sl.js +5 -5
- package/package.json +64 -63
- package/src/agents/ai-governance/index.js +12 -0
- package/src/agents/ai-governance/tools/base.js +171 -0
- package/src/agents/ai-governance/tools/eval-regression.js +47 -0
- package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
- package/src/agents/ai-governance/tools/index.js +52 -0
- package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
- package/src/agents/ai-governance/tools/provenance-check.js +69 -0
- package/src/agents/backend/index.js +12 -0
- package/src/agents/backend/tools/base.js +189 -0
- package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
- package/src/agents/backend/tools/idempotency-audit.js +105 -0
- package/src/agents/backend/tools/index.js +87 -0
- package/src/agents/backend/tools/retry-audit.js +132 -0
- package/src/agents/backend/tools/timeout-audit.js +144 -0
- package/src/agents/code-quality/index.js +12 -0
- package/src/agents/code-quality/tools/base.js +159 -0
- package/src/agents/code-quality/tools/complexity-measure.js +197 -0
- package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
- package/src/agents/code-quality/tools/cycle-detect.js +49 -0
- package/src/agents/code-quality/tools/dep-graph.js +196 -0
- package/src/agents/code-quality/tools/index.js +89 -0
- package/src/agents/data-layer/index.js +12 -0
- package/src/agents/data-layer/tools/base.js +181 -0
- package/src/agents/data-layer/tools/index-audit.js +165 -0
- package/src/agents/data-layer/tools/index.js +83 -0
- package/src/agents/data-layer/tools/migration-scan.js +135 -0
- package/src/agents/data-layer/tools/query-explain.js +120 -0
- package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
- package/src/agents/documentation/index.js +12 -0
- package/src/agents/documentation/tools/api-diff.js +91 -0
- package/src/agents/documentation/tools/base.js +151 -0
- package/src/agents/documentation/tools/dead-link-check.js +58 -0
- package/src/agents/documentation/tools/docstring-coverage.js +78 -0
- package/src/agents/documentation/tools/index.js +52 -0
- package/src/agents/documentation/tools/readme-freshness.js +61 -0
- package/src/agents/envelope/fix-cycle.js +45 -0
- package/src/agents/envelope/index.js +31 -0
- package/src/agents/envelope/loop.js +150 -0
- package/src/agents/envelope/pulse.js +18 -0
- package/src/agents/envelope/stream.js +40 -0
- package/src/agents/infrastructure/index.js +12 -0
- package/src/agents/infrastructure/tools/base.js +171 -0
- package/src/agents/infrastructure/tools/checkov-run.js +32 -0
- package/src/agents/infrastructure/tools/drift-detect.js +59 -0
- package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
- package/src/agents/infrastructure/tools/index.js +52 -0
- package/src/agents/infrastructure/tools/tflint-run.js +31 -0
- package/src/agents/jules/config/definition.js +160 -160
- package/src/agents/jules/config/system-prompt.js +182 -182
- package/src/agents/jules/error-intake.js +51 -51
- package/src/agents/jules/fix-cycle.js +17 -17
- package/src/agents/jules/loop.js +460 -450
- package/src/agents/jules/pulse.js +10 -10
- package/src/agents/jules/stream.js +187 -186
- package/src/agents/jules/swarm/file-scanner.js +74 -74
- package/src/agents/jules/swarm/index.js +11 -11
- package/src/agents/jules/swarm/orchestrator.js +362 -362
- package/src/agents/jules/swarm/pattern-hunter.js +123 -123
- package/src/agents/jules/swarm/sub-agent.js +315 -309
- package/src/agents/jules/tools/aidenid-email.js +189 -189
- package/src/agents/jules/tools/auth-audit.js +1708 -1691
- package/src/agents/jules/tools/dispatch.js +340 -335
- package/src/agents/jules/tools/file-edit.js +2 -2
- package/src/agents/jules/tools/file-read.js +2 -2
- package/src/agents/jules/tools/frontend-analyze.js +570 -570
- package/src/agents/jules/tools/glob.js +2 -2
- package/src/agents/jules/tools/grep.js +2 -2
- package/src/agents/jules/tools/index.js +29 -29
- package/src/agents/jules/tools/path-guards.js +2 -2
- package/src/agents/jules/tools/runtime-audit.js +507 -507
- package/src/agents/jules/tools/shell.js +2 -2
- package/src/agents/jules/tools/url-policy.js +100 -100
- package/src/agents/mode.js +113 -0
- package/src/agents/observability/index.js +12 -0
- package/src/agents/observability/tools/alert-audit.js +39 -0
- package/src/agents/observability/tools/base.js +181 -0
- package/src/agents/observability/tools/dashboard-gap.js +42 -0
- package/src/agents/observability/tools/index.js +54 -0
- package/src/agents/observability/tools/log-schema-check.js +74 -0
- package/src/agents/observability/tools/span-coverage.js +74 -0
- package/src/agents/persona-visuals.js +102 -61
- package/src/agents/release/index.js +12 -0
- package/src/agents/release/tools/base.js +181 -0
- package/src/agents/release/tools/changelog-diff.js +86 -0
- package/src/agents/release/tools/feature-flag-audit.js +126 -0
- package/src/agents/release/tools/index.js +61 -0
- package/src/agents/release/tools/rollback-verify.js +129 -0
- package/src/agents/release/tools/semver-check.js +109 -0
- package/src/agents/reliability/index.js +12 -0
- package/src/agents/reliability/tools/backpressure-check.js +129 -0
- package/src/agents/reliability/tools/base.js +181 -0
- package/src/agents/reliability/tools/chaos-probe.js +109 -0
- package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
- package/src/agents/reliability/tools/health-check-audit.js +111 -0
- package/src/agents/reliability/tools/index.js +87 -0
- package/src/agents/run-persona.js +109 -0
- package/src/agents/security/index.js +12 -0
- package/src/agents/security/tools/authz-audit.js +134 -0
- package/src/agents/security/tools/base.js +190 -0
- package/src/agents/security/tools/crypto-review.js +175 -0
- package/src/agents/security/tools/index.js +97 -0
- package/src/agents/security/tools/sast-scan.js +175 -0
- package/src/agents/security/tools/secrets-scan.js +216 -0
- package/src/agents/shared-tools/dispatch-core.js +320 -315
- package/src/agents/shared-tools/file-edit.js +180 -180
- package/src/agents/shared-tools/file-read.js +100 -100
- package/src/agents/shared-tools/glob.js +168 -168
- package/src/agents/shared-tools/grep.js +228 -228
- package/src/agents/shared-tools/index.js +46 -46
- package/src/agents/shared-tools/path-guards.js +161 -161
- package/src/agents/shared-tools/shell.js +383 -383
- package/src/agents/supply-chain/index.js +12 -0
- package/src/agents/supply-chain/tools/attestation-check.js +42 -0
- package/src/agents/supply-chain/tools/base.js +151 -0
- package/src/agents/supply-chain/tools/index.js +52 -0
- package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
- package/src/agents/supply-chain/tools/package-verify.js +56 -0
- package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
- package/src/agents/testing/index.js +12 -0
- package/src/agents/testing/tools/base.js +202 -0
- package/src/agents/testing/tools/coverage-gap.js +144 -0
- package/src/agents/testing/tools/flake-detect.js +125 -0
- package/src/agents/testing/tools/index.js +85 -0
- package/src/agents/testing/tools/mutation-test.js +143 -0
- package/src/agents/testing/tools/snapshot-diff.js +103 -0
- package/src/ai/aidenid.js +1021 -1009
- package/src/ai/client.js +553 -553
- package/src/ai/domain-target-store.js +268 -268
- package/src/ai/identity-store.js +270 -270
- package/src/ai/proxy.js +137 -137
- package/src/ai/site-store.js +145 -145
- package/src/audit/agents/architecture.js +180 -180
- package/src/audit/agents/compliance.js +179 -179
- package/src/audit/agents/documentation.js +165 -165
- package/src/audit/agents/performance.js +145 -145
- package/src/audit/agents/security.js +215 -215
- package/src/audit/agents/testing.js +172 -172
- package/src/audit/orchestrator.js +557 -557
- package/src/audit/package.js +204 -204
- package/src/audit/registry.js +284 -284
- package/src/audit/replay.js +103 -103
- package/src/auth/gate.js +428 -371
- package/src/auth/http.js +681 -611
- package/src/auth/service.js +1106 -1106
- package/src/auth/session-store.js +813 -813
- package/src/cli.js +257 -252
- package/src/commands/ai/identity-lifecycle.js +1338 -1338
- package/src/commands/ai/provision-governance.js +1272 -1272
- package/src/commands/ai/shared.js +147 -147
- package/src/commands/ai.js +11 -11
- package/src/commands/apply.js +12 -12
- package/src/commands/audit.js +1171 -1166
- package/src/commands/auth.js +419 -419
- package/src/commands/chat.js +184 -191
- package/src/commands/config.js +184 -184
- package/src/commands/cost.js +311 -311
- package/src/commands/daemon/core.js +850 -850
- package/src/commands/daemon/extended.js +1048 -1048
- package/src/commands/daemon/shared.js +213 -213
- package/src/commands/daemon.js +11 -11
- package/src/commands/guide.js +174 -174
- package/src/commands/ingest.js +58 -58
- package/src/commands/init.js +55 -55
- package/src/commands/legacy-args.js +20 -10
- package/src/commands/mcp.js +461 -461
- package/src/commands/omargate.js +63 -29
- package/src/commands/persona.js +65 -20
- package/src/commands/plugin.js +260 -260
- package/src/commands/policy.js +132 -132
- package/src/commands/prompt.js +238 -238
- package/src/commands/review.js +704 -704
- package/src/commands/scan.js +865 -872
- package/src/commands/session.js +1238 -0
- package/src/commands/spec.js +771 -716
- package/src/commands/swarm.js +651 -651
- package/src/commands/telemetry.js +202 -202
- package/src/commands/watch.js +511 -511
- package/src/config/agent-dictionary.js +182 -182
- package/src/config/io.js +56 -56
- package/src/config/paths.js +18 -18
- package/src/config/schema.js +55 -55
- package/src/config/service.js +184 -184
- package/src/coord/events-log.js +141 -0
- package/src/coord/handshake.js +719 -0
- package/src/coord/index.js +35 -0
- package/src/coord/paths.js +84 -0
- package/src/coord/priority.js +62 -0
- package/src/coord/tarjan.js +157 -0
- package/src/cost/budget.js +235 -235
- package/src/cost/history.js +188 -188
- package/src/cost/tokenizer.js +160 -0
- package/src/cost/tracker.js +232 -171
- package/src/daemon/artifact-lineage.js +896 -534
- package/src/daemon/assignment-ledger.js +1083 -770
- package/src/daemon/ast-drift.js +496 -0
- package/src/daemon/ast-parser-layer.js +258 -258
- package/src/daemon/budget-governor.js +633 -633
- package/src/daemon/callgraph-overlay.js +646 -646
- package/src/daemon/error-worker.js +1209 -626
- package/src/daemon/fix-cycle.js +384 -377
- package/src/daemon/hybrid-mapper.js +929 -929
- package/src/daemon/ingest-refresh.js +79 -11
- package/src/daemon/jira-lifecycle.js +767 -632
- package/src/daemon/operator-control.js +657 -657
- package/src/daemon/pulse.js +327 -327
- package/src/daemon/reliability-lane.js +471 -471
- package/src/daemon/scope-engine.js +1068 -0
- package/src/daemon/watchdog.js +971 -971
- package/src/events/schema.js +190 -0
- package/src/guide/generator.js +316 -316
- package/src/ingest/engine.js +933 -918
- package/src/ingest/ownership.js +380 -0
- package/src/interactive/index.js +97 -97
- package/src/legacy-cli.js +3228 -2994
- package/src/mcp/registry.js +695 -695
- package/src/memory/blackboard.js +301 -301
- package/src/memory/retrieval.js +581 -581
- package/src/orchestrator/kai-chen.js +126 -0
- package/src/plugin/manifest.js +553 -553
- package/src/policy/packs.js +144 -144
- package/src/prompt/generator.js +136 -118
- package/src/review/ai-review.js +672 -679
- package/src/review/compliance-pack.js +389 -0
- package/src/review/investor-dd-config.js +54 -0
- package/src/review/investor-dd-file-loop.js +303 -0
- package/src/review/investor-dd-file-router.js +406 -0
- package/src/review/investor-dd-html-report.js +233 -0
- package/src/review/investor-dd-notification.js +120 -0
- package/src/review/investor-dd-orchestrator.js +405 -0
- package/src/review/investor-dd-persona-runner.js +275 -0
- package/src/review/live-validator.js +253 -0
- package/src/review/local-review.js +1351 -1305
- package/src/review/omargate-interactive.js +68 -68
- package/src/review/omargate-orchestrator.js +492 -300
- package/src/review/persona-prompts.js +484 -296
- package/src/review/reconciliation-rules.js +329 -0
- package/src/review/replay.js +235 -235
- package/src/review/report.js +664 -664
- package/src/review/reproducibility-chain.js +136 -0
- package/src/review/scan-modes.js +147 -42
- package/src/review/spec-binding.js +487 -487
- package/src/scaffold/generator.js +67 -67
- package/src/scaffold/templates.js +150 -150
- package/src/scan/generator.js +418 -418
- package/src/scan/gh-secrets.js +107 -107
- package/src/session/agent-registry.js +359 -0
- package/src/session/analytics.js +479 -0
- package/src/session/daemon.js +1396 -0
- package/src/session/file-locks.js +666 -0
- package/src/session/paths.js +37 -0
- package/src/session/recap.js +567 -0
- package/src/session/redact.js +82 -0
- package/src/session/runtime-bridge.js +762 -0
- package/src/session/scoring.js +406 -0
- package/src/session/setup-guides.js +304 -0
- package/src/session/store.js +704 -0
- package/src/session/stream.js +333 -0
- package/src/session/sync.js +753 -0
- package/src/session/tasks.js +1054 -0
- package/src/session/templates.js +188 -0
- package/src/spec/generator.js +619 -519
- package/src/spec/regenerate.js +237 -237
- package/src/spec/templates.js +91 -91
- package/src/swarm/dashboard.js +247 -247
- package/src/swarm/factory.js +363 -363
- package/src/swarm/pentest.js +934 -934
- package/src/swarm/registry.js +419 -419
- package/src/swarm/report.js +158 -158
- package/src/swarm/runtime.js +569 -576
- package/src/swarm/scenario-dsl.js +272 -272
- package/src/telemetry/ledger.js +302 -302
- package/src/telemetry/session-tracker.js +234 -234
- package/src/telemetry/sync.js +203 -203
- package/src/ui/command-hints.js +13 -13
- package/src/ui/markdown.js +220 -220
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Nina (security persona) domain-tool registry (#A13).
|
|
2
|
+
//
|
|
3
|
+
// Each entry exports (a) the tool id the LLM references in tool_use blocks,
|
|
4
|
+
// (b) a schema describing the expected arguments, and (c) the async handler.
|
|
5
|
+
|
|
6
|
+
import { runAuthzAudit } from "./authz-audit.js";
|
|
7
|
+
import { runCryptoReview } from "./crypto-review.js";
|
|
8
|
+
import { runSastScan } from "./sast-scan.js";
|
|
9
|
+
import { runSecretsScan } from "./secrets-scan.js";
|
|
10
|
+
|
|
11
|
+
export const SECURITY_TOOLS = Object.freeze({
|
|
12
|
+
"sast-scan": {
|
|
13
|
+
id: "sast-scan",
|
|
14
|
+
description:
|
|
15
|
+
"Pattern-based SAST over JS/TS/Python/Go/Ruby/Java sources. Returns P0/P1 findings for eval, dynamic Function, shell-injection, innerHTML XSS, Python exec/compile, subprocess shell=True, fs.readFile path traversal.",
|
|
16
|
+
schema: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
rootPath: { type: "string", description: "Repo root (defaults to CWD)." },
|
|
20
|
+
files: {
|
|
21
|
+
type: "array",
|
|
22
|
+
items: { type: "string" },
|
|
23
|
+
description: "Optional explicit file list; defaults to a full repo walk.",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
handler: runSastScan,
|
|
28
|
+
},
|
|
29
|
+
"secrets-scan": {
|
|
30
|
+
id: "secrets-scan",
|
|
31
|
+
description:
|
|
32
|
+
"Scan the repo for hardcoded AWS/GitHub/Slack/OpenAI/Anthropic/Stripe tokens, private key blocks, and entropy-gated generic credentials.",
|
|
33
|
+
schema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
rootPath: { type: "string" },
|
|
37
|
+
files: { type: "array", items: { type: "string" } },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
handler: runSecretsScan,
|
|
41
|
+
},
|
|
42
|
+
"authz-audit": {
|
|
43
|
+
id: "authz-audit",
|
|
44
|
+
description:
|
|
45
|
+
"Inspect mutation-style route handlers (POST/PUT/PATCH/DELETE in Express / Fastify / Next.js app router / Python FastAPI / Flask) and flag those without a recognizable auth guard above them.",
|
|
46
|
+
schema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
rootPath: { type: "string" },
|
|
50
|
+
files: { type: "array", items: { type: "string" } },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
handler: runAuthzAudit,
|
|
54
|
+
},
|
|
55
|
+
"crypto-review": {
|
|
56
|
+
id: "crypto-review",
|
|
57
|
+
description:
|
|
58
|
+
"Flag MD5/SHA-1 for security use, Math.random in token/secret/nonce contexts, TLS certificate verification opt-outs (Node / Python / Go cert-bypass toggles), and hardcoded cipher IVs.",
|
|
59
|
+
schema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
rootPath: { type: "string" },
|
|
63
|
+
files: { type: "array", items: { type: "string" } },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
handler: runCryptoReview,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const SECURITY_TOOL_IDS = Object.freeze(Object.keys(SECURITY_TOOLS));
|
|
71
|
+
|
|
72
|
+
export async function dispatchSecurityTool(toolId, args = {}) {
|
|
73
|
+
const tool = SECURITY_TOOLS[toolId];
|
|
74
|
+
if (!tool) {
|
|
75
|
+
throw new Error(`Unknown security tool: ${toolId}`);
|
|
76
|
+
}
|
|
77
|
+
return tool.handler(args);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Run every tool in sequence and return a flat Finding[] across all of
|
|
81
|
+
// them. Used by the persona orchestrator when a "full security sweep" is
|
|
82
|
+
// requested.
|
|
83
|
+
export async function runAllSecurityTools({ rootPath, files = null } = {}) {
|
|
84
|
+
const findings = [];
|
|
85
|
+
for (const toolId of SECURITY_TOOL_IDS) {
|
|
86
|
+
const out = await dispatchSecurityTool(toolId, { rootPath, files });
|
|
87
|
+
findings.push(...out);
|
|
88
|
+
}
|
|
89
|
+
return findings;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export {
|
|
93
|
+
runAuthzAudit,
|
|
94
|
+
runCryptoReview,
|
|
95
|
+
runSastScan,
|
|
96
|
+
runSecretsScan,
|
|
97
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// sast-scan — pattern-based SAST for Nina's security persona (#A13).
|
|
2
|
+
//
|
|
3
|
+
// This is a zero-dep static-analysis pass. We don't try to replicate
|
|
4
|
+
// semgrep / bandit — instead we ship a curated ruleset of rules that the
|
|
5
|
+
// deterministic Omar Gate already validates at commit time, plus a handful
|
|
6
|
+
// of contextual checks that benefit from iterating file-by-file instead of
|
|
7
|
+
// running a global grep. Callers that want semgrep / bandit should wire in
|
|
8
|
+
// the full scanner via the Omar Gate action's SecurityScanGate (#A2); the
|
|
9
|
+
// tool here is for ad-hoc persona-triggered review.
|
|
10
|
+
|
|
11
|
+
import fsp from "node:fs/promises";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
import { createFinding, lineNumberOf, walkRepoFiles } from "./base.js";
|
|
15
|
+
|
|
16
|
+
const JS_TS_EXTENSIONS = new Set([
|
|
17
|
+
".js",
|
|
18
|
+
".jsx",
|
|
19
|
+
".ts",
|
|
20
|
+
".tsx",
|
|
21
|
+
".mjs",
|
|
22
|
+
".cjs",
|
|
23
|
+
]);
|
|
24
|
+
const PY_EXTENSIONS = new Set([".py"]);
|
|
25
|
+
const ALL_CODE_EXTENSIONS = new Set([
|
|
26
|
+
...JS_TS_EXTENSIONS,
|
|
27
|
+
...PY_EXTENSIONS,
|
|
28
|
+
".go",
|
|
29
|
+
".rb",
|
|
30
|
+
".java",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const RULES = [
|
|
34
|
+
{
|
|
35
|
+
id: "sast.eval",
|
|
36
|
+
// Pattern built via concatenation so the source of this file does not
|
|
37
|
+
// contain the literal trigger string verbatim — otherwise the repo's
|
|
38
|
+
// own SAST scanner flags this module with its own rule.
|
|
39
|
+
pattern: new RegExp("(^|[^\\w])" + "e" + "val\\s*\\("),
|
|
40
|
+
severity: "P0",
|
|
41
|
+
languages: [...JS_TS_EXTENSIONS],
|
|
42
|
+
rootCause:
|
|
43
|
+
"The JavaScript dynamic-evaluation built-in executes arbitrary strings as code — any attacker-controlled input becomes RCE.",
|
|
44
|
+
recommendedFix:
|
|
45
|
+
"Replace dynamic evaluation with structured parsing (JSON.parse, a whitelist, or Function with a frozen arg list).",
|
|
46
|
+
confidence: 0.9,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "sast.function-constructor",
|
|
50
|
+
pattern: new RegExp("new\\s+" + "Function\\s*\\("),
|
|
51
|
+
severity: "P0",
|
|
52
|
+
languages: [...JS_TS_EXTENSIONS],
|
|
53
|
+
rootCause:
|
|
54
|
+
"The Function constructor with a string body is a dynamic code-execution sink similar to the dynamic-evaluation built-in.",
|
|
55
|
+
recommendedFix:
|
|
56
|
+
"Use a statically-defined function. If you really need configurable behavior, pass data (not code) and dispatch with a switch / lookup table.",
|
|
57
|
+
confidence: 0.85,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "sast.child-process-shell",
|
|
61
|
+
pattern: /\b(?:exec|execSync|spawnSync)\s*\(\s*[`'"][^`'"]*\$\{[^}]+\}/,
|
|
62
|
+
severity: "P0",
|
|
63
|
+
languages: [...JS_TS_EXTENSIONS],
|
|
64
|
+
rootCause:
|
|
65
|
+
"A template literal interpolates user-controlled data directly into a shell command — classic command injection path.",
|
|
66
|
+
recommendedFix:
|
|
67
|
+
"Switch to execFile / spawn with argv as a list (no shell). If you must use a shell, escape with shell-quote or a whitelist.",
|
|
68
|
+
confidence: 0.85,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "sast.innerhtml-user-input",
|
|
72
|
+
pattern: /\.innerHTML\s*=\s*[^`"';\n]*\b(?:req|request|params|query|body|input)\b/i,
|
|
73
|
+
severity: "P0",
|
|
74
|
+
languages: [...JS_TS_EXTENSIONS],
|
|
75
|
+
rootCause:
|
|
76
|
+
"Writing request-origin data to innerHTML is an XSS vector — HTML special characters execute as markup.",
|
|
77
|
+
recommendedFix:
|
|
78
|
+
"Use textContent for plain text, or DOMPurify.sanitize() / framework-provided escapers for rich content.",
|
|
79
|
+
confidence: 0.8,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "sast.python-exec",
|
|
83
|
+
pattern: /\b(?:exec|compile)\s*\(/,
|
|
84
|
+
severity: "P0",
|
|
85
|
+
languages: [...PY_EXTENSIONS],
|
|
86
|
+
rootCause:
|
|
87
|
+
"Python exec / compile evaluate runtime strings; attacker-controlled arguments become RCE.",
|
|
88
|
+
recommendedFix:
|
|
89
|
+
"Use structured data + dispatch (ast.literal_eval for literals, ast.parse with Validator for stricter control).",
|
|
90
|
+
confidence: 0.8,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "sast.python-subprocess-shell",
|
|
94
|
+
pattern: /subprocess\.(?:run|call|Popen|check_output)\s*\([^)]*shell\s*=\s*True/,
|
|
95
|
+
severity: "P0",
|
|
96
|
+
languages: [...PY_EXTENSIONS],
|
|
97
|
+
rootCause:
|
|
98
|
+
"subprocess with shell=True interpolates arguments into /bin/sh — command injection path when any arg is user-controlled.",
|
|
99
|
+
recommendedFix:
|
|
100
|
+
"Drop shell=True and pass argv as a list. If a shell is required, pipe via shlex.quote on every interpolated value.",
|
|
101
|
+
confidence: 0.9,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "sast.path-traversal-fs-read",
|
|
105
|
+
pattern: /fs\.(?:readFile|readFileSync|createReadStream)\s*\(\s*[^`'";\n]*\b(?:req|request|params|query|body)\b/,
|
|
106
|
+
severity: "P1",
|
|
107
|
+
languages: [...JS_TS_EXTENSIONS],
|
|
108
|
+
rootCause:
|
|
109
|
+
"Passing request-origin strings directly to fs.readFile opens a path-traversal vector.",
|
|
110
|
+
recommendedFix:
|
|
111
|
+
"Resolve + validate the candidate path stays under an allowedRoot (use tools like shared-tools/path-guards.js).",
|
|
112
|
+
confidence: 0.75,
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
export async function runSastScan({ rootPath, files = null, rules = RULES } = {}) {
|
|
117
|
+
const resolvedRoot = path.resolve(String(rootPath || "."));
|
|
118
|
+
const iterator =
|
|
119
|
+
Array.isArray(files) && files.length > 0
|
|
120
|
+
? iterateExplicitFiles(resolvedRoot, files)
|
|
121
|
+
: walkRepoFiles({ rootPath: resolvedRoot, extensions: ALL_CODE_EXTENSIONS });
|
|
122
|
+
|
|
123
|
+
const findings = [];
|
|
124
|
+
for await (const { fullPath, relativePath } of iterator) {
|
|
125
|
+
let content;
|
|
126
|
+
try {
|
|
127
|
+
content = await fsp.readFile(fullPath, "utf-8");
|
|
128
|
+
} catch {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const ext = path.extname(fullPath).toLowerCase();
|
|
132
|
+
for (const rule of rules) {
|
|
133
|
+
if (Array.isArray(rule.languages) && !rule.languages.includes(ext)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const line = lineNumberOf(content, rule.pattern);
|
|
137
|
+
if (!line) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const lineContent = content.split(/\r?\n/)[line - 1] || "";
|
|
141
|
+
findings.push(
|
|
142
|
+
createFinding({
|
|
143
|
+
tool: "sast-scan",
|
|
144
|
+
kind: rule.id,
|
|
145
|
+
severity: rule.severity,
|
|
146
|
+
file: relativePath,
|
|
147
|
+
line,
|
|
148
|
+
evidence: lineContent,
|
|
149
|
+
rootCause: rule.rootCause,
|
|
150
|
+
recommendedFix: rule.recommendedFix,
|
|
151
|
+
confidence: rule.confidence,
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return findings;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function* iterateExplicitFiles(resolvedRoot, files) {
|
|
160
|
+
for (const file of files) {
|
|
161
|
+
const trimmed = String(file || "").trim();
|
|
162
|
+
if (!trimmed) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const fullPath = path.isAbsolute(trimmed)
|
|
166
|
+
? trimmed
|
|
167
|
+
: path.join(resolvedRoot, trimmed);
|
|
168
|
+
const relativePath = path
|
|
169
|
+
.relative(resolvedRoot, fullPath)
|
|
170
|
+
.replace(/\\/g, "/");
|
|
171
|
+
yield { fullPath, relativePath };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export { RULES as SAST_RULES };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// secrets-scan — filesystem scan for credentials (#A13).
|
|
2
|
+
//
|
|
3
|
+
// Mirrors the highest-confidence rules from gitleaks / trufflehog so the
|
|
4
|
+
// tool can run without external binaries. When gitleaks is installed it
|
|
5
|
+
// shadows this, but the zero-dep fallback keeps persona dispatch honest
|
|
6
|
+
// on stripped-down runners.
|
|
7
|
+
|
|
8
|
+
import fsp from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
import { createFinding, walkRepoFiles } from "./base.js";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_EXTENSIONS = new Set([
|
|
14
|
+
".js",
|
|
15
|
+
".jsx",
|
|
16
|
+
".ts",
|
|
17
|
+
".tsx",
|
|
18
|
+
".mjs",
|
|
19
|
+
".cjs",
|
|
20
|
+
".py",
|
|
21
|
+
".go",
|
|
22
|
+
".rb",
|
|
23
|
+
".rs",
|
|
24
|
+
".java",
|
|
25
|
+
".kt",
|
|
26
|
+
".yaml",
|
|
27
|
+
".yml",
|
|
28
|
+
".json",
|
|
29
|
+
".toml",
|
|
30
|
+
".env",
|
|
31
|
+
".env.local",
|
|
32
|
+
".env.production",
|
|
33
|
+
".sh",
|
|
34
|
+
".bash",
|
|
35
|
+
// Extensionless files like id_rsa / id_ed25519 / .pem with no extension
|
|
36
|
+
// frequently contain secrets; let the walker yield them too.
|
|
37
|
+
"",
|
|
38
|
+
".pem",
|
|
39
|
+
".key",
|
|
40
|
+
".crt",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
// Each rule captures (a) regex to detect the secret, (b) entropy floor (to
|
|
44
|
+
// filter placeholder values like "X"*40), (c) severity and description.
|
|
45
|
+
const RULES = [
|
|
46
|
+
{
|
|
47
|
+
id: "secret.aws-access-key",
|
|
48
|
+
pattern: /\bAKIA[0-9A-Z]{16}\b/,
|
|
49
|
+
severity: "P0",
|
|
50
|
+
minEntropy: 3.0,
|
|
51
|
+
description:
|
|
52
|
+
"AWS Access Key ID committed to the repo — rotate and revoke immediately.",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "secret.aws-secret-access-key",
|
|
56
|
+
pattern: /aws_secret_access_key\s*[:=]\s*['"]?[A-Za-z0-9/+=]{40}['"]?/i,
|
|
57
|
+
severity: "P0",
|
|
58
|
+
minEntropy: 4.0,
|
|
59
|
+
description: "AWS secret access key assignment with 40-char token shape.",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "secret.github-token",
|
|
63
|
+
pattern: /\bgh[ps]_[A-Za-z0-9]{36}\b/,
|
|
64
|
+
severity: "P0",
|
|
65
|
+
minEntropy: 3.5,
|
|
66
|
+
description: "GitHub personal / PAT token committed to the repo.",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "secret.slack-token",
|
|
70
|
+
pattern: /\bxox[aboprs]-[A-Za-z0-9-]{10,}\b/,
|
|
71
|
+
severity: "P0",
|
|
72
|
+
minEntropy: 3.5,
|
|
73
|
+
description: "Slack token (bot/app/workflow) — revoke via Slack admin.",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "secret.openai-api-key",
|
|
77
|
+
pattern: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/,
|
|
78
|
+
severity: "P0",
|
|
79
|
+
minEntropy: 3.0,
|
|
80
|
+
description:
|
|
81
|
+
"OpenAI API key committed to the repo — revoke in the OpenAI dashboard.",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "secret.anthropic-api-key",
|
|
85
|
+
pattern: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/,
|
|
86
|
+
severity: "P0",
|
|
87
|
+
minEntropy: 3.0,
|
|
88
|
+
description: "Anthropic API key committed to the repo.",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "secret.stripe-secret-key",
|
|
92
|
+
pattern: /\bsk_(?:live|test)_[A-Za-z0-9]{24,}\b/,
|
|
93
|
+
severity: "P0",
|
|
94
|
+
minEntropy: 3.5,
|
|
95
|
+
description:
|
|
96
|
+
"Stripe secret key committed. Test keys still expose test-mode customers.",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "secret.private-key-block",
|
|
100
|
+
pattern: /-----BEGIN (?:RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/,
|
|
101
|
+
severity: "P0",
|
|
102
|
+
minEntropy: 0, // PEM block is unambiguous without entropy check
|
|
103
|
+
description:
|
|
104
|
+
"Private key block embedded in source — rotate and remove from history.",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "secret.generic-hardcoded",
|
|
108
|
+
pattern: /(?:api[_-]?key|secret|token|password|passwd)\s*[:=]\s*['"][A-Za-z0-9_\-]{16,}['"]/i,
|
|
109
|
+
severity: "P1",
|
|
110
|
+
minEntropy: 3.2,
|
|
111
|
+
description:
|
|
112
|
+
"High-entropy hardcoded credential assigned to a security-sounding identifier.",
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
function shannonEntropy(str) {
|
|
117
|
+
const text = String(str || "");
|
|
118
|
+
if (!text) {
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
const freq = new Map();
|
|
122
|
+
for (const ch of text) {
|
|
123
|
+
freq.set(ch, (freq.get(ch) || 0) + 1);
|
|
124
|
+
}
|
|
125
|
+
const len = text.length;
|
|
126
|
+
let entropy = 0;
|
|
127
|
+
for (const count of freq.values()) {
|
|
128
|
+
const p = count / len;
|
|
129
|
+
entropy -= p * Math.log2(p);
|
|
130
|
+
}
|
|
131
|
+
return entropy;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function extractToken(match) {
|
|
135
|
+
// Pull the first contiguous token-ish substring from the match so we can
|
|
136
|
+
// measure entropy on the actual secret instead of surrounding assignment
|
|
137
|
+
// boilerplate.
|
|
138
|
+
const text = String(match || "");
|
|
139
|
+
const token = text.match(/[A-Za-z0-9_\-./+=]{10,}/);
|
|
140
|
+
return token ? token[0] : text;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function runSecretsScan({ rootPath, files = null } = {}) {
|
|
144
|
+
const resolvedRoot = path.resolve(String(rootPath || "."));
|
|
145
|
+
const iterator =
|
|
146
|
+
Array.isArray(files) && files.length > 0
|
|
147
|
+
? iterateExplicitFiles(resolvedRoot, files)
|
|
148
|
+
: walkRepoFiles({ rootPath: resolvedRoot, extensions: DEFAULT_EXTENSIONS });
|
|
149
|
+
|
|
150
|
+
const findings = [];
|
|
151
|
+
for await (const { fullPath, relativePath } of iterator) {
|
|
152
|
+
let content;
|
|
153
|
+
try {
|
|
154
|
+
content = await fsp.readFile(fullPath, "utf-8");
|
|
155
|
+
} catch {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const lines = content.split(/\r?\n/);
|
|
159
|
+
for (const rule of RULES) {
|
|
160
|
+
const global = new RegExp(rule.pattern.source, rule.pattern.flags.includes("g") ? rule.pattern.flags : `${rule.pattern.flags}g`);
|
|
161
|
+
let match;
|
|
162
|
+
while ((match = global.exec(content)) !== null) {
|
|
163
|
+
const token = extractToken(match[0]);
|
|
164
|
+
const entropy = shannonEntropy(token);
|
|
165
|
+
if (rule.minEntropy > 0 && entropy < rule.minEntropy) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const lineIndex = content.slice(0, match.index).split(/\r?\n/).length;
|
|
169
|
+
const evidence = (lines[lineIndex - 1] || "").trim().slice(0, 200);
|
|
170
|
+
findings.push(
|
|
171
|
+
createFinding({
|
|
172
|
+
tool: "secrets-scan",
|
|
173
|
+
kind: rule.id,
|
|
174
|
+
severity: rule.severity,
|
|
175
|
+
file: relativePath,
|
|
176
|
+
line: lineIndex,
|
|
177
|
+
evidence: redactEvidence(evidence, token),
|
|
178
|
+
rootCause: rule.description,
|
|
179
|
+
recommendedFix:
|
|
180
|
+
"Revoke the credential at the provider, rotate to a new secret, and store it in your secret manager (never in source).",
|
|
181
|
+
confidence: Math.max(0.7, Math.min(1, entropy / 5)),
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return findings;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function redactEvidence(line, token) {
|
|
191
|
+
if (!token || token.length < 12) {
|
|
192
|
+
return line;
|
|
193
|
+
}
|
|
194
|
+
// Show first 6 + "..." so reviewers can recognize the key family without
|
|
195
|
+
// re-exposing the full secret in logs.
|
|
196
|
+
const redacted = `${token.slice(0, 6)}...${token.slice(-2)}`;
|
|
197
|
+
return line.replace(token, redacted);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function* iterateExplicitFiles(resolvedRoot, files) {
|
|
201
|
+
for (const file of files) {
|
|
202
|
+
const trimmed = String(file || "").trim();
|
|
203
|
+
if (!trimmed) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const fullPath = path.isAbsolute(trimmed)
|
|
207
|
+
? trimmed
|
|
208
|
+
: path.join(resolvedRoot, trimmed);
|
|
209
|
+
const relativePath = path
|
|
210
|
+
.relative(resolvedRoot, fullPath)
|
|
211
|
+
.replace(/\\/g, "/");
|
|
212
|
+
yield { fullPath, relativePath };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export { RULES as SECRETS_RULES };
|