sentinelayer-cli 0.8.0 → 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 +13 -0
- package/package.json +4 -4
- 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/loop.js +7 -4
- package/src/agents/jules/swarm/sub-agent.js +5 -1
- package/src/agents/jules/tools/auth-audit.js +10 -1
- 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 +38 -0
- 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/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/auth/gate.js +65 -37
- package/src/cli.js +1 -1
- package/src/commands/chat.js +3 -10
- package/src/commands/legacy-args.js +10 -0
- package/src/commands/omargate.js +36 -2
- package/src/commands/persona.js +46 -1
- package/src/commands/scan.js +3 -10
- package/src/commands/session.js +654 -6
- package/src/commands/spec.js +3 -10
- 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/tokenizer.js +160 -0
- package/src/cost/tracker.js +61 -0
- package/src/daemon/artifact-lineage.js +362 -0
- package/src/daemon/assignment-ledger.js +117 -0
- package/src/daemon/ast-drift.js +496 -0
- package/src/daemon/ingest-refresh.js +69 -2
- package/src/ingest/engine.js +15 -0
- package/src/ingest/ownership.js +380 -0
- package/src/legacy-cli.js +68 -1
- package/src/orchestrator/kai-chen.js +126 -0
- package/src/review/ai-review.js +3 -10
- 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/omargate-orchestrator.js +90 -2
- package/src/review/persona-prompts.js +244 -56
- package/src/review/reconciliation-rules.js +329 -0
- package/src/review/reproducibility-chain.js +136 -0
- package/src/review/scan-modes.js +102 -3
- package/src/session/agent-registry.js +7 -0
- package/src/session/analytics.js +479 -0
- package/src/session/daemon.js +609 -14
- package/src/session/file-locks.js +666 -0
- package/src/session/paths.js +4 -0
- package/src/session/recap.js +567 -0
- package/src/session/redact.js +82 -0
- package/src/session/runtime-bridge.js +24 -1
- package/src/session/scoring.js +406 -0
- package/src/session/setup-guides.js +304 -0
- package/src/session/store.js +318 -2
- package/src/session/stream.js +9 -1
- package/src/session/sync.js +753 -0
- package/src/session/tasks.js +1054 -0
- package/src/session/templates.js +188 -0
- package/src/swarm/runtime.js +1 -8
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Noah (reliability persona) domain-tool registry (#A18).
|
|
2
|
+
|
|
3
|
+
import { runBackpressureCheck } from "./backpressure-check.js";
|
|
4
|
+
import { runChaosProbe } from "./chaos-probe.js";
|
|
5
|
+
import { runGracefulDegradationCheck } from "./graceful-degradation-check.js";
|
|
6
|
+
import { runHealthCheckAudit } from "./health-check-audit.js";
|
|
7
|
+
|
|
8
|
+
export const RELIABILITY_TOOLS = Object.freeze({
|
|
9
|
+
"chaos-probe": {
|
|
10
|
+
id: "chaos-probe",
|
|
11
|
+
description:
|
|
12
|
+
"Report whether the repo has any chaos-testing / fault-injection signals (chaostoolkit, chaos-monkey-js, LitmusChaos, gremlin). Only advises when the repo has ≥3 outbound-HTTP call sites.",
|
|
13
|
+
schema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
rootPath: { type: "string" },
|
|
17
|
+
files: { type: "array", items: { type: "string" } },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
handler: runChaosProbe,
|
|
21
|
+
},
|
|
22
|
+
"health-check-audit": {
|
|
23
|
+
id: "health-check-audit",
|
|
24
|
+
description:
|
|
25
|
+
"Flag service directories that declare HTTP routes but don't expose /health /healthz /ready /live / _status.",
|
|
26
|
+
schema: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
rootPath: { type: "string" },
|
|
30
|
+
files: { type: "array", items: { type: "string" } },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
handler: runHealthCheckAudit,
|
|
34
|
+
},
|
|
35
|
+
"graceful-degradation-check": {
|
|
36
|
+
id: "graceful-degradation-check",
|
|
37
|
+
description:
|
|
38
|
+
"Flag files that make outbound HTTP calls without try/catch, cached fallback, feature-flag, or circuit-breaker in scope.",
|
|
39
|
+
schema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
rootPath: { type: "string" },
|
|
43
|
+
files: { type: "array", items: { type: "string" } },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
handler: runGracefulDegradationCheck,
|
|
47
|
+
},
|
|
48
|
+
"backpressure-check": {
|
|
49
|
+
id: "backpressure-check",
|
|
50
|
+
description:
|
|
51
|
+
"Find queue/worker consumers (Bull, Kafka, SQS, RabbitMQ, Redis pub/sub, Celery) and flag those missing concurrency caps or DLQ / retry-limit configuration.",
|
|
52
|
+
schema: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
rootPath: { type: "string" },
|
|
56
|
+
files: { type: "array", items: { type: "string" } },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
handler: runBackpressureCheck,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const RELIABILITY_TOOL_IDS = Object.freeze(Object.keys(RELIABILITY_TOOLS));
|
|
64
|
+
|
|
65
|
+
export async function dispatchReliabilityTool(toolId, args = {}) {
|
|
66
|
+
const tool = RELIABILITY_TOOLS[toolId];
|
|
67
|
+
if (!tool) {
|
|
68
|
+
throw new Error(`Unknown reliability tool: ${toolId}`);
|
|
69
|
+
}
|
|
70
|
+
return tool.handler(args);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function runAllReliabilityTools({ rootPath, files = null } = {}) {
|
|
74
|
+
const findings = [];
|
|
75
|
+
for (const toolId of RELIABILITY_TOOL_IDS) {
|
|
76
|
+
const out = await dispatchReliabilityTool(toolId, { rootPath, files });
|
|
77
|
+
findings.push(...out);
|
|
78
|
+
}
|
|
79
|
+
return findings;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export {
|
|
83
|
+
runBackpressureCheck,
|
|
84
|
+
runChaosProbe,
|
|
85
|
+
runGracefulDegradationCheck,
|
|
86
|
+
runHealthCheckAudit,
|
|
87
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// runPersona — single-persona execution driver (#A27 runtime integration).
|
|
2
|
+
//
|
|
3
|
+
// Takes a persona id + mode and runs that persona's domain-tool sweep over
|
|
4
|
+
// the given repo root / file list. Today this is what's wired to the CLI
|
|
5
|
+
// `persona run <id>` and to omargate.persona_dispatch.
|
|
6
|
+
//
|
|
7
|
+
// - Audit mode (default): invokes the persona's `runAll<X>Tools` and
|
|
8
|
+
// returns the resulting Finding[]. No LLM call, no file writes.
|
|
9
|
+
// - Codegen mode: runs the same tool sweep AND attaches the mode config
|
|
10
|
+
// from `agents/mode.js` (allowed-tools + prompt suffix) to the result.
|
|
11
|
+
// The actual LLM spawn + edit loop happens in the caller — this driver
|
|
12
|
+
// only produces the deterministic baseline + plan envelope.
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
buildPersonaConfigForMode,
|
|
16
|
+
listKnownPersonaIds,
|
|
17
|
+
normalizePersonaMode,
|
|
18
|
+
} from "./mode.js";
|
|
19
|
+
|
|
20
|
+
// Lazy-load each persona's module to avoid paying the import cost for
|
|
21
|
+
// every persona on every invocation. Each entry is a thunk that returns
|
|
22
|
+
// the persona's runAll* function.
|
|
23
|
+
const PERSONA_LOADERS = Object.freeze({
|
|
24
|
+
"ai-governance": async () =>
|
|
25
|
+
(await import("./ai-governance/index.js")).runAllAiGovernanceTools,
|
|
26
|
+
"backend": async () =>
|
|
27
|
+
(await import("./backend/index.js")).runAllBackendTools,
|
|
28
|
+
"code-quality": async () =>
|
|
29
|
+
(await import("./code-quality/index.js")).runAllCodeQualityTools,
|
|
30
|
+
"data-layer": async () =>
|
|
31
|
+
(await import("./data-layer/index.js")).runAllDataLayerTools,
|
|
32
|
+
"documentation": async () =>
|
|
33
|
+
(await import("./documentation/index.js")).runAllDocumentationTools,
|
|
34
|
+
"infrastructure": async () =>
|
|
35
|
+
(await import("./infrastructure/index.js")).runAllInfrastructureTools,
|
|
36
|
+
"observability": async () =>
|
|
37
|
+
(await import("./observability/index.js")).runAllObservabilityTools,
|
|
38
|
+
"release": async () =>
|
|
39
|
+
(await import("./release/index.js")).runAllReleaseTools,
|
|
40
|
+
"reliability": async () =>
|
|
41
|
+
(await import("./reliability/index.js")).runAllReliabilityTools,
|
|
42
|
+
"security": async () =>
|
|
43
|
+
(await import("./security/index.js")).runAllSecurityTools,
|
|
44
|
+
"supply-chain": async () =>
|
|
45
|
+
(await import("./supply-chain/index.js")).runAllSupplyChainTools,
|
|
46
|
+
"testing": async () =>
|
|
47
|
+
(await import("./testing/index.js")).runAllTestingTools,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const SUPPORTED_PERSONA_IDS = Object.freeze(
|
|
51
|
+
Object.keys(PERSONA_LOADERS).sort()
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
function normalizePersonaId(personaId) {
|
|
55
|
+
return String(personaId || "").trim().toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeFiles(files) {
|
|
59
|
+
if (!files) return [];
|
|
60
|
+
if (Array.isArray(files)) {
|
|
61
|
+
return files.map((f) => String(f || "").trim()).filter(Boolean);
|
|
62
|
+
}
|
|
63
|
+
return String(files)
|
|
64
|
+
.split(",")
|
|
65
|
+
.map((f) => f.trim())
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function runPersona({
|
|
70
|
+
personaId,
|
|
71
|
+
mode = "audit",
|
|
72
|
+
rootPath,
|
|
73
|
+
files = null,
|
|
74
|
+
} = {}) {
|
|
75
|
+
const id = normalizePersonaId(personaId);
|
|
76
|
+
if (!id) {
|
|
77
|
+
throw new Error("personaId is required.");
|
|
78
|
+
}
|
|
79
|
+
if (!PERSONA_LOADERS[id]) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Unknown persona id: ${personaId}. Supported: ${SUPPORTED_PERSONA_IDS.join(", ")}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const normalizedMode = normalizePersonaMode(mode);
|
|
85
|
+
const normalizedFiles = normalizeFiles(files);
|
|
86
|
+
const loader = PERSONA_LOADERS[id];
|
|
87
|
+
const runAllTools = await loader();
|
|
88
|
+
|
|
89
|
+
const toolFiles = normalizedFiles.length > 0 ? normalizedFiles : null;
|
|
90
|
+
const findings = await runAllTools({
|
|
91
|
+
rootPath: String(rootPath || "."),
|
|
92
|
+
files: toolFiles,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const modeConfig = buildPersonaConfigForMode(id, normalizedMode);
|
|
96
|
+
return {
|
|
97
|
+
personaId: id,
|
|
98
|
+
mode: normalizedMode,
|
|
99
|
+
rootPath: String(rootPath || "."),
|
|
100
|
+
files: normalizedFiles,
|
|
101
|
+
findings: Array.isArray(findings) ? findings : [],
|
|
102
|
+
mode_config: {
|
|
103
|
+
allowedTools: modeConfig.allowedTools,
|
|
104
|
+
promptSuffix: modeConfig.promptSuffix,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { listKnownPersonaIds };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// authz-audit — look for route handlers that forget to call auth middleware (#A13).
|
|
2
|
+
//
|
|
3
|
+
// This is a lightweight static pass. We don't try to model every framework —
|
|
4
|
+
// we focus on the three routing styles the DevTestBot substrate actually
|
|
5
|
+
// uses (Express, Fastify, Next.js app-router route handlers) plus a Python
|
|
6
|
+
// FastAPI pass.
|
|
7
|
+
//
|
|
8
|
+
// Strategy: for each detected route declaration, look at the 6-line window
|
|
9
|
+
// above for an auth/session guard. If none is present, emit a P1 finding
|
|
10
|
+
// with moderate confidence — the persona LLM layer (or a human reviewer)
|
|
11
|
+
// decides whether it's a real gap.
|
|
12
|
+
|
|
13
|
+
import fsp from "node:fs/promises";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
|
|
16
|
+
import { createFinding, walkRepoFiles } from "./base.js";
|
|
17
|
+
|
|
18
|
+
const JS_TS_EXTENSIONS = new Set([
|
|
19
|
+
".js",
|
|
20
|
+
".jsx",
|
|
21
|
+
".ts",
|
|
22
|
+
".tsx",
|
|
23
|
+
".mjs",
|
|
24
|
+
".cjs",
|
|
25
|
+
]);
|
|
26
|
+
const PY_EXTENSIONS = new Set([".py"]);
|
|
27
|
+
|
|
28
|
+
// Patterns that make us comfortable that the route IS guarded.
|
|
29
|
+
const AUTH_GUARD_PATTERNS = [
|
|
30
|
+
/requireAuth|requireSession|requireUser|requireLogin|auth\.\w+|authenticate|isAuthenticated|ensureAuthenticated|protect\(/,
|
|
31
|
+
/@login_required|@require_auth|@protected|HTTPBearer|Depends\(get_current_user/,
|
|
32
|
+
/middleware:\s*\[[^\]]*auth/i,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// Route declaration patterns we consider "mutation-ish" (POST/PUT/PATCH/DELETE).
|
|
36
|
+
const JS_ROUTE_PATTERNS = [
|
|
37
|
+
/\b(?:app|router|route|server)\.(post|put|patch|delete)\s*\(/,
|
|
38
|
+
/\bfastify\.(post|put|patch|delete)\s*\(/,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Next.js app-router POST / PUT / DELETE / PATCH handler declarations.
|
|
42
|
+
const NEXT_APP_ROUTER_PATTERNS = [
|
|
43
|
+
/^export\s+async\s+function\s+(POST|PUT|PATCH|DELETE)\s*\(/m,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const PY_ROUTE_PATTERNS = [
|
|
47
|
+
/@(?:app|router)\.(post|put|patch|delete)\s*\(/,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function hasGuardAbove(lines, idx, window = 6) {
|
|
51
|
+
const start = Math.max(0, idx - window);
|
|
52
|
+
const snippet = lines.slice(start, idx + 1).join("\n");
|
|
53
|
+
return AUTH_GUARD_PATTERNS.some((pattern) => pattern.test(snippet));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function evidenceForRoute(lines, idx) {
|
|
57
|
+
const start = Math.max(0, idx - 1);
|
|
58
|
+
const end = Math.min(lines.length - 1, idx + 2);
|
|
59
|
+
return lines.slice(start, end + 1).join("\n").trim().slice(0, 300);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function runAuthzAudit({ rootPath, files = null } = {}) {
|
|
63
|
+
const resolvedRoot = path.resolve(String(rootPath || "."));
|
|
64
|
+
const extensions = new Set([...JS_TS_EXTENSIONS, ...PY_EXTENSIONS]);
|
|
65
|
+
const iterator =
|
|
66
|
+
Array.isArray(files) && files.length > 0
|
|
67
|
+
? iterateExplicitFiles(resolvedRoot, files)
|
|
68
|
+
: walkRepoFiles({ rootPath: resolvedRoot, extensions });
|
|
69
|
+
|
|
70
|
+
const findings = [];
|
|
71
|
+
for await (const { fullPath, relativePath } of iterator) {
|
|
72
|
+
let content;
|
|
73
|
+
try {
|
|
74
|
+
content = await fsp.readFile(fullPath, "utf-8");
|
|
75
|
+
} catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const ext = path.extname(fullPath).toLowerCase();
|
|
79
|
+
const lines = content.split(/\r?\n/);
|
|
80
|
+
const routePatterns =
|
|
81
|
+
PY_EXTENSIONS.has(ext) ? PY_ROUTE_PATTERNS : [...JS_ROUTE_PATTERNS, ...NEXT_APP_ROUTER_PATTERNS];
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
84
|
+
const line = lines[i];
|
|
85
|
+
let matchedRoute = null;
|
|
86
|
+
for (const pattern of routePatterns) {
|
|
87
|
+
if (pattern.test(line)) {
|
|
88
|
+
matchedRoute = line;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!matchedRoute) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (hasGuardAbove(lines, i)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
findings.push(
|
|
99
|
+
createFinding({
|
|
100
|
+
tool: "authz-audit",
|
|
101
|
+
kind: "authz.missing-guard",
|
|
102
|
+
severity: "P1",
|
|
103
|
+
file: relativePath,
|
|
104
|
+
line: i + 1,
|
|
105
|
+
evidence: evidenceForRoute(lines, i),
|
|
106
|
+
rootCause:
|
|
107
|
+
"A mutation-style route handler was declared without a recognizable auth guard in the 6 lines above it.",
|
|
108
|
+
recommendedFix:
|
|
109
|
+
"Add a middleware / decorator that validates the caller's session (requireAuth, @login_required, Depends(get_current_user), …) before the handler body runs.",
|
|
110
|
+
confidence: 0.55,
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return findings;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function* iterateExplicitFiles(resolvedRoot, files) {
|
|
119
|
+
for (const file of files) {
|
|
120
|
+
const trimmed = String(file || "").trim();
|
|
121
|
+
if (!trimmed) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const fullPath = path.isAbsolute(trimmed)
|
|
125
|
+
? trimmed
|
|
126
|
+
: path.join(resolvedRoot, trimmed);
|
|
127
|
+
const relativePath = path
|
|
128
|
+
.relative(resolvedRoot, fullPath)
|
|
129
|
+
.replace(/\\/g, "/");
|
|
130
|
+
yield { fullPath, relativePath };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { AUTH_GUARD_PATTERNS };
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Shared helpers for Nina's (security) domain tools (#A13).
|
|
2
|
+
//
|
|
3
|
+
// Every security tool returns the same Finding shape so the orchestrator can
|
|
4
|
+
// bin / merge results without case analysis. We also centralize the file
|
|
5
|
+
// walker and scope filter here so no tool re-invents the ignore logic.
|
|
6
|
+
|
|
7
|
+
import fsp from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import process from "node:process";
|
|
10
|
+
|
|
11
|
+
import ignore from "ignore";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_IGNORED_DIRS = new Set([
|
|
14
|
+
".git",
|
|
15
|
+
"node_modules",
|
|
16
|
+
".venv",
|
|
17
|
+
".next",
|
|
18
|
+
"dist",
|
|
19
|
+
"build",
|
|
20
|
+
"coverage",
|
|
21
|
+
".sentinelayer",
|
|
22
|
+
".sentinel",
|
|
23
|
+
".turbo",
|
|
24
|
+
".idea",
|
|
25
|
+
".vscode",
|
|
26
|
+
"__pycache__",
|
|
27
|
+
".cache",
|
|
28
|
+
]);
|
|
29
|
+
const MAX_FILE_SIZE_BYTES = 1024 * 1024;
|
|
30
|
+
const SEVERITIES = Object.freeze(["P0", "P1", "P2", "P3"]);
|
|
31
|
+
|
|
32
|
+
export function toPosix(value) {
|
|
33
|
+
return String(value || "").replace(/\\/g, "/");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function normalizeSeverity(value) {
|
|
37
|
+
const normalized = String(value || "").trim().toUpperCase();
|
|
38
|
+
if (SEVERITIES.includes(normalized)) {
|
|
39
|
+
return normalized;
|
|
40
|
+
}
|
|
41
|
+
return "P2";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Canonical Finding shape — every security tool returns objects matching
|
|
45
|
+
// this schema.
|
|
46
|
+
export function createFinding({
|
|
47
|
+
severity,
|
|
48
|
+
kind,
|
|
49
|
+
file,
|
|
50
|
+
line = 0,
|
|
51
|
+
evidence = "",
|
|
52
|
+
rootCause = "",
|
|
53
|
+
recommendedFix = "",
|
|
54
|
+
confidence = null,
|
|
55
|
+
tool = "",
|
|
56
|
+
persona = "security",
|
|
57
|
+
} = {}) {
|
|
58
|
+
return {
|
|
59
|
+
persona,
|
|
60
|
+
tool: String(tool || "").trim(),
|
|
61
|
+
kind: String(kind || "").trim() || "security",
|
|
62
|
+
severity: normalizeSeverity(severity),
|
|
63
|
+
file: toPosix(file || ""),
|
|
64
|
+
line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
|
|
65
|
+
evidence: String(evidence || "").trim().slice(0, 400),
|
|
66
|
+
rootCause: String(rootCause || "").trim(),
|
|
67
|
+
recommendedFix: String(recommendedFix || "").trim(),
|
|
68
|
+
confidence:
|
|
69
|
+
confidence === null || confidence === undefined
|
|
70
|
+
? null
|
|
71
|
+
: Math.max(0, Math.min(1, Number(confidence) || 0)),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function readIgnorePatterns(filePath) {
|
|
76
|
+
try {
|
|
77
|
+
const raw = await fsp.readFile(filePath, "utf-8");
|
|
78
|
+
return String(raw || "")
|
|
79
|
+
.split(/\r?\n/)
|
|
80
|
+
.map((line) => line.trim())
|
|
81
|
+
.filter((line) => line && !line.startsWith("#"));
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (err && typeof err === "object" && err.code === "ENOENT") {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function createIgnoreMatcher(rootPath) {
|
|
91
|
+
const matcher = ignore();
|
|
92
|
+
const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
|
|
93
|
+
const sentinel = await readIgnorePatterns(
|
|
94
|
+
path.join(rootPath, ".sentinelayerignore")
|
|
95
|
+
);
|
|
96
|
+
matcher.add([...gitignore, ...sentinel]);
|
|
97
|
+
return (relativePath, isDirectory) => {
|
|
98
|
+
const normalized = toPosix(relativePath);
|
|
99
|
+
if (!normalized) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const candidate = isDirectory ? `${normalized}/` : normalized;
|
|
103
|
+
return matcher.ignores(candidate);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Walk the target repo, yielding files whose extension is in `extensions`
|
|
108
|
+
// (or all files when `extensions` is empty). Respects .gitignore +
|
|
109
|
+
// .sentinelayerignore + DEFAULT_IGNORED_DIRS.
|
|
110
|
+
export async function* walkRepoFiles({
|
|
111
|
+
rootPath = process.cwd(),
|
|
112
|
+
extensions = new Set(),
|
|
113
|
+
maxFileSize = MAX_FILE_SIZE_BYTES,
|
|
114
|
+
} = {}) {
|
|
115
|
+
const resolvedRoot = path.resolve(rootPath);
|
|
116
|
+
const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
|
|
117
|
+
const wantedExtensions =
|
|
118
|
+
extensions instanceof Set
|
|
119
|
+
? extensions
|
|
120
|
+
: new Set(Array.isArray(extensions) ? extensions : []);
|
|
121
|
+
const stack = [resolvedRoot];
|
|
122
|
+
|
|
123
|
+
while (stack.length > 0) {
|
|
124
|
+
const current = stack.pop();
|
|
125
|
+
let entries = [];
|
|
126
|
+
try {
|
|
127
|
+
entries = await fsp.readdir(current, { withFileTypes: true });
|
|
128
|
+
} catch {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
const fullPath = path.join(current, entry.name);
|
|
133
|
+
const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
|
|
134
|
+
if (entry.isDirectory()) {
|
|
135
|
+
if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (ignoreMatcher(relativePath, true)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
stack.push(fullPath);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!entry.isFile()) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (ignoreMatcher(relativePath, false)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
151
|
+
if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
let stat = null;
|
|
155
|
+
try {
|
|
156
|
+
stat = await fsp.stat(fullPath);
|
|
157
|
+
} catch {
|
|
158
|
+
stat = null;
|
|
159
|
+
}
|
|
160
|
+
if (!stat || stat.size > maxFileSize) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
yield { fullPath, relativePath };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Find line number of the first occurrence of a pattern in `content`.
|
|
169
|
+
// Returns 0 when not found.
|
|
170
|
+
export function lineNumberOf(content, pattern) {
|
|
171
|
+
const text = String(content || "");
|
|
172
|
+
if (!pattern) {
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
const idx = text.search(pattern);
|
|
176
|
+
if (idx < 0) {
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
const before = text.slice(0, idx);
|
|
180
|
+
return before.split(/\r?\n/).length;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Capture one matching line (trimmed) for evidence.
|
|
184
|
+
export function evidenceAroundMatch(content, line) {
|
|
185
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
186
|
+
const idx = Math.max(0, (Number(line) || 1) - 1);
|
|
187
|
+
return (lines[idx] || "").trim();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export { DEFAULT_IGNORED_DIRS, MAX_FILE_SIZE_BYTES, SEVERITIES };
|