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
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
// Re-export from shared tools. Shell is not Jules-specific.
|
|
2
|
-
export { shell, analyzeCommand, buildScrubbedEnv, ShellError, ShellBlockedError } from "../../shared-tools/shell.js";
|
|
1
|
+
// Re-export from shared tools. Shell is not Jules-specific.
|
|
2
|
+
export { shell, analyzeCommand, buildScrubbedEnv, ShellError, ShellBlockedError } from "../../shared-tools/shell.js";
|
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
const PRIVATE_HOST_SUFFIXES = [".internal", ".local", ".localhost"];
|
|
2
|
-
const BLOCKED_LITERAL_HOSTS = new Set([
|
|
3
|
-
"localhost",
|
|
4
|
-
"127.0.0.1",
|
|
5
|
-
"::1",
|
|
6
|
-
"0.0.0.0",
|
|
7
|
-
"169.254.169.254",
|
|
8
|
-
"metadata.google.internal",
|
|
9
|
-
"metadata.google.internal.",
|
|
10
|
-
]);
|
|
11
|
-
|
|
12
|
-
function isNumericIpv4(hostname) {
|
|
13
|
-
const parts = String(hostname || "").split(".");
|
|
14
|
-
if (parts.length !== 4) {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
return parts.every((part) => /^[0-9]{1,3}$/.test(part) && Number(part) >= 0 && Number(part) <= 255);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function isPrivateIpv4(hostname) {
|
|
21
|
-
if (!isNumericIpv4(hostname)) {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
const parts = hostname.split(".").map((part) => Number(part));
|
|
25
|
-
const [a, b] = parts;
|
|
26
|
-
if (a === 10 || a === 127 || a === 0) return true;
|
|
27
|
-
if (a === 169 && b === 254) return true;
|
|
28
|
-
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
29
|
-
if (a === 192 && b === 168) return true;
|
|
30
|
-
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
31
|
-
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isPrivateIpv6(hostname) {
|
|
36
|
-
const normalized = String(hostname || "").toLowerCase().split("%")[0];
|
|
37
|
-
if (!normalized.includes(":")) {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
if (normalized === "::1" || normalized === "::") {
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
if (normalized.startsWith("fc") || normalized.startsWith("fd")) {
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
if (normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb")) {
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function isPrivateHostname(hostname) {
|
|
53
|
-
const normalized = String(hostname || "").toLowerCase();
|
|
54
|
-
if (!normalized) {
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
if (BLOCKED_LITERAL_HOSTS.has(normalized)) {
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
if (PRIVATE_HOST_SUFFIXES.some((suffix) => normalized.endsWith(suffix))) {
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
if (isPrivateIpv4(normalized) || isPrivateIpv6(normalized)) {
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function isPrivateTargetBypassEnabled(allowPrivateTargets) {
|
|
70
|
-
if (allowPrivateTargets === true) {
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
if (process.env.SENTINELAYER_ALLOW_PRIVATE_AUDIT_TARGETS === "1") {
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
if (process.env.NODE_ENV === "test") {
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function assertPermittedAuditTarget(urlValue, options = {}) {
|
|
83
|
-
const { operation = "audit", allowPrivateTargets = false } = options;
|
|
84
|
-
let parsed;
|
|
85
|
-
try {
|
|
86
|
-
parsed = new URL(urlValue);
|
|
87
|
-
} catch {
|
|
88
|
-
throw new Error("Invalid URL: " + urlValue);
|
|
89
|
-
}
|
|
90
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
91
|
-
throw new Error("Invalid URL: " + parsed.toString());
|
|
92
|
-
}
|
|
93
|
-
if (!isPrivateTargetBypassEnabled(allowPrivateTargets) && isPrivateHostname(parsed.hostname)) {
|
|
94
|
-
throw new Error(
|
|
95
|
-
`Blocked private audit target for ${operation}: ${parsed.hostname}. ` +
|
|
96
|
-
"Set allowPrivateTargets=true or SENTINELAYER_ALLOW_PRIVATE_AUDIT_TARGETS=1 to override."
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
return parsed;
|
|
100
|
-
}
|
|
1
|
+
const PRIVATE_HOST_SUFFIXES = [".internal", ".local", ".localhost"];
|
|
2
|
+
const BLOCKED_LITERAL_HOSTS = new Set([
|
|
3
|
+
"localhost",
|
|
4
|
+
"127.0.0.1",
|
|
5
|
+
"::1",
|
|
6
|
+
"0.0.0.0",
|
|
7
|
+
"169.254.169.254",
|
|
8
|
+
"metadata.google.internal",
|
|
9
|
+
"metadata.google.internal.",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function isNumericIpv4(hostname) {
|
|
13
|
+
const parts = String(hostname || "").split(".");
|
|
14
|
+
if (parts.length !== 4) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return parts.every((part) => /^[0-9]{1,3}$/.test(part) && Number(part) >= 0 && Number(part) <= 255);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isPrivateIpv4(hostname) {
|
|
21
|
+
if (!isNumericIpv4(hostname)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const parts = hostname.split(".").map((part) => Number(part));
|
|
25
|
+
const [a, b] = parts;
|
|
26
|
+
if (a === 10 || a === 127 || a === 0) return true;
|
|
27
|
+
if (a === 169 && b === 254) return true;
|
|
28
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
29
|
+
if (a === 192 && b === 168) return true;
|
|
30
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
31
|
+
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isPrivateIpv6(hostname) {
|
|
36
|
+
const normalized = String(hostname || "").toLowerCase().split("%")[0];
|
|
37
|
+
if (!normalized.includes(":")) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (normalized === "::1" || normalized === "::") {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (normalized.startsWith("fc") || normalized.startsWith("fd")) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb")) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isPrivateHostname(hostname) {
|
|
53
|
+
const normalized = String(hostname || "").toLowerCase();
|
|
54
|
+
if (!normalized) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (BLOCKED_LITERAL_HOSTS.has(normalized)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (PRIVATE_HOST_SUFFIXES.some((suffix) => normalized.endsWith(suffix))) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (isPrivateIpv4(normalized) || isPrivateIpv6(normalized)) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isPrivateTargetBypassEnabled(allowPrivateTargets) {
|
|
70
|
+
if (allowPrivateTargets === true) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (process.env.SENTINELAYER_ALLOW_PRIVATE_AUDIT_TARGETS === "1") {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
if (process.env.NODE_ENV === "test") {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function assertPermittedAuditTarget(urlValue, options = {}) {
|
|
83
|
+
const { operation = "audit", allowPrivateTargets = false } = options;
|
|
84
|
+
let parsed;
|
|
85
|
+
try {
|
|
86
|
+
parsed = new URL(urlValue);
|
|
87
|
+
} catch {
|
|
88
|
+
throw new Error("Invalid URL: " + urlValue);
|
|
89
|
+
}
|
|
90
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
91
|
+
throw new Error("Invalid URL: " + parsed.toString());
|
|
92
|
+
}
|
|
93
|
+
if (!isPrivateTargetBypassEnabled(allowPrivateTargets) && isPrivateHostname(parsed.hostname)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Blocked private audit target for ${operation}: ${parsed.hostname}. ` +
|
|
96
|
+
"Set allowPrivateTargets=true or SENTINELAYER_ALLOW_PRIVATE_AUDIT_TARGETS=1 to override."
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Persona mode selector (#A27, spec §Phase 5 / PR #A27).
|
|
2
|
+
//
|
|
3
|
+
// The persona envelope (PR #A8) is identical between audit and code-gen
|
|
4
|
+
// modes. The only per-mode variation:
|
|
5
|
+
// 1. The allowed-tools subset. Audit gets read-only tools + domain scans.
|
|
6
|
+
// Codegen gets the read-only tools PLUS file-edit + shell (sandboxed)
|
|
7
|
+
// so the persona can apply fixes.
|
|
8
|
+
// 2. The system-prompt suffix. Audit mode appends "Emit findings only.
|
|
9
|
+
// Do not modify files." Codegen mode appends "Apply fixes as minimal
|
|
10
|
+
// edits with clear commit-message summaries."
|
|
11
|
+
//
|
|
12
|
+
// This module is the single source of truth for those two deltas. Every
|
|
13
|
+
// caller that spawns a persona does so through `buildPersonaConfigForMode`;
|
|
14
|
+
// same persona, same domain tools, same budget knobs — only the two deltas
|
|
15
|
+
// change.
|
|
16
|
+
|
|
17
|
+
// Persona tool id lists are inlined here instead of imported from each
|
|
18
|
+
// persona module. Rationale: this module has to be importable before all
|
|
19
|
+
// 12 persona PRs (#A13-A24) have merged to main. Once they do, a future
|
|
20
|
+
// refactor can swap these for real imports — the tool-id strings are
|
|
21
|
+
// stable anyway.
|
|
22
|
+
|
|
23
|
+
export const PERSONA_MODES = Object.freeze(["audit", "codegen"]);
|
|
24
|
+
|
|
25
|
+
// Read-only tool ids that exist in every persona — the audit baseline.
|
|
26
|
+
// When a persona is invoked in `audit` mode, it gets the union of its own
|
|
27
|
+
// domain tools and these shared scanners. Code-gen mode adds the edit /
|
|
28
|
+
// shell tools below on top.
|
|
29
|
+
const READONLY_BASELINE = Object.freeze([
|
|
30
|
+
"FileRead",
|
|
31
|
+
"Grep",
|
|
32
|
+
"Glob",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const CODEGEN_EXTRA_TOOLS = Object.freeze([
|
|
36
|
+
"FileEdit",
|
|
37
|
+
"Shell",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const DOMAIN_TOOL_IDS_BY_PERSONA = Object.freeze({
|
|
41
|
+
"ai-governance": Object.freeze(["eval-regression", "hitl-audit", "prompt-drift", "provenance-check"]),
|
|
42
|
+
"backend": Object.freeze(["circuit-breaker-check", "idempotency-audit", "retry-audit", "timeout-audit"]),
|
|
43
|
+
"code-quality": Object.freeze(["complexity-measure", "coupling-analysis", "cycle-detect", "dep-graph"]),
|
|
44
|
+
"data-layer": Object.freeze(["index-audit", "migration-scan", "query-explain", "tenancy-scan"]),
|
|
45
|
+
"documentation": Object.freeze(["api-diff", "dead-link-check", "docstring-coverage", "readme-freshness"]),
|
|
46
|
+
"infrastructure": Object.freeze(["checkov-run", "drift-detect", "iam-least-priv-check", "tflint-run"]),
|
|
47
|
+
"observability": Object.freeze(["alert-audit", "dashboard-gap", "log-schema-check", "span-coverage"]),
|
|
48
|
+
"release": Object.freeze(["changelog-diff", "feature-flag-audit", "rollback-verify", "semver-check"]),
|
|
49
|
+
"reliability": Object.freeze(["backpressure-check", "chaos-probe", "graceful-degradation-check", "health-check-audit"]),
|
|
50
|
+
"security": Object.freeze(["authz-audit", "crypto-review", "sast-scan", "secrets-scan"]),
|
|
51
|
+
"supply-chain": Object.freeze(["attestation-check", "lockfile-integrity", "package-verify", "sbom-diff"]),
|
|
52
|
+
"testing": Object.freeze(["coverage-gap", "flake-detect", "mutation-test", "snapshot-diff"]),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const MODE_PROMPT_SUFFIXES = Object.freeze({
|
|
56
|
+
audit: [
|
|
57
|
+
"",
|
|
58
|
+
"You are operating in AUDIT mode.",
|
|
59
|
+
"Emit findings only. Do not modify files. Your output is structured JSON that downstream tooling will rank, dedupe, and present to the reviewer.",
|
|
60
|
+
"When you call a domain tool, the tool writes Finding objects. You decide which of those to elevate into your final report and how to prioritize them.",
|
|
61
|
+
].join("\n"),
|
|
62
|
+
codegen: [
|
|
63
|
+
"",
|
|
64
|
+
"You are operating in CODE-GEN mode.",
|
|
65
|
+
"Apply fixes as minimal, reviewable edits. Every file you touch must compile / parse; every change should include a one-line commit-message-style rationale.",
|
|
66
|
+
"When you call a domain tool, treat the resulting Finding list as the work queue. Your output is a set of file edits plus an explanation of what you did and what you deliberately left for a human.",
|
|
67
|
+
].join("\n"),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export function normalizePersonaMode(mode) {
|
|
71
|
+
const normalized = String(mode || "").trim().toLowerCase();
|
|
72
|
+
if (PERSONA_MODES.includes(normalized)) {
|
|
73
|
+
return normalized;
|
|
74
|
+
}
|
|
75
|
+
return "audit";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Return { mode, allowedTools, promptSuffix, personaId } for a persona in
|
|
79
|
+
// the requested mode. The only values that depend on mode are allowedTools
|
|
80
|
+
// and promptSuffix; everything else (budget, identity, system prompt body)
|
|
81
|
+
// is supplied by the persona's own definition.
|
|
82
|
+
export function buildPersonaConfigForMode(personaId, mode) {
|
|
83
|
+
const normalizedMode = normalizePersonaMode(mode);
|
|
84
|
+
const normalizedPersona = String(personaId || "").trim().toLowerCase();
|
|
85
|
+
const domainTools = DOMAIN_TOOL_IDS_BY_PERSONA[normalizedPersona] || [];
|
|
86
|
+
|
|
87
|
+
const allowedTools = new Set([...READONLY_BASELINE, ...domainTools]);
|
|
88
|
+
if (normalizedMode === "codegen") {
|
|
89
|
+
for (const tool of CODEGEN_EXTRA_TOOLS) {
|
|
90
|
+
allowedTools.add(tool);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
personaId: normalizedPersona,
|
|
95
|
+
mode: normalizedMode,
|
|
96
|
+
allowedTools: Array.from(allowedTools),
|
|
97
|
+
promptSuffix: MODE_PROMPT_SUFFIXES[normalizedMode],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Quick boolean for callers that just want "can this mode write files?"
|
|
102
|
+
export function modeAllowsWrites(mode) {
|
|
103
|
+
return normalizePersonaMode(mode) === "codegen";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Which personas does the mode selector recognize? Useful for tests /
|
|
107
|
+
// diagnostics — callers can detect a typo in a persona id before they
|
|
108
|
+
// dispatch.
|
|
109
|
+
export function listKnownPersonaIds() {
|
|
110
|
+
return Object.keys(DOMAIN_TOOL_IDS_BY_PERSONA).slice().sort();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { READONLY_BASELINE, CODEGEN_EXTRA_TOOLS, MODE_PROMPT_SUFFIXES };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Sofia (observability persona) — barrel export (#A20).
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
OBSERVABILITY_TOOLS,
|
|
5
|
+
OBSERVABILITY_TOOL_IDS,
|
|
6
|
+
dispatchObservabilityTool,
|
|
7
|
+
runAllObservabilityTools,
|
|
8
|
+
runAlertAudit,
|
|
9
|
+
runDashboardGap,
|
|
10
|
+
runLogSchemaCheck,
|
|
11
|
+
runSpanCoverage,
|
|
12
|
+
} from "./tools/index.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// alert-audit — check that alert definitions exist (#A20).
|
|
2
|
+
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { createFinding, toPosix, walkRepoFiles } from "./base.js";
|
|
7
|
+
|
|
8
|
+
const ALERT_SIGNATURES = [
|
|
9
|
+
/(^|\/)alerts?\/[^/]+\.(ya?ml|json|tf)$/i,
|
|
10
|
+
/(^|\/)prometheus\/rules?\//i,
|
|
11
|
+
/(^|\/)alertmanager\//i,
|
|
12
|
+
/(^|\/)monitors?\/[^/]+\.(ya?ml|json|tf)$/i,
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export async function runAlertAudit({ rootPath } = {}) {
|
|
16
|
+
const resolvedRoot = path.resolve(String(rootPath || "."));
|
|
17
|
+
let found = false;
|
|
18
|
+
for await (const { relativePath } of walkRepoFiles({ rootPath: resolvedRoot })) {
|
|
19
|
+
const rel = toPosix(relativePath);
|
|
20
|
+
if (ALERT_SIGNATURES.some((p) => p.test(rel))) {
|
|
21
|
+
found = true;
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (found) return [];
|
|
26
|
+
return [
|
|
27
|
+
createFinding({
|
|
28
|
+
tool: "alert-audit",
|
|
29
|
+
kind: "observability.no-alerts",
|
|
30
|
+
severity: "P2",
|
|
31
|
+
file: "",
|
|
32
|
+
line: 0,
|
|
33
|
+
evidence: "No alert definitions found under alerts/, monitors/, prometheus/rules/, or alertmanager/",
|
|
34
|
+
rootCause: "Without declarative alert definitions we can't tell what production failures the team is actually notified of.",
|
|
35
|
+
recommendedFix: "Define alerts in code (Prometheus rules, Datadog Terraform monitors, SLO burn-rate alerts). Require every critical endpoint to have an error-rate + latency alert.",
|
|
36
|
+
confidence: 0.6,
|
|
37
|
+
}),
|
|
38
|
+
];
|
|
39
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Shared helpers for Sofia's (observability) domain tools (#A20).
|
|
2
|
+
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
|
|
7
|
+
import ignore from "ignore";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_IGNORED_DIRS = new Set([
|
|
10
|
+
".git",
|
|
11
|
+
"node_modules",
|
|
12
|
+
".venv",
|
|
13
|
+
".next",
|
|
14
|
+
"dist",
|
|
15
|
+
"build",
|
|
16
|
+
"coverage",
|
|
17
|
+
".sentinelayer",
|
|
18
|
+
".sentinel",
|
|
19
|
+
".turbo",
|
|
20
|
+
".idea",
|
|
21
|
+
".vscode",
|
|
22
|
+
"__pycache__",
|
|
23
|
+
".cache",
|
|
24
|
+
]);
|
|
25
|
+
const MAX_FILE_SIZE_BYTES = 1024 * 1024;
|
|
26
|
+
const SEVERITIES = Object.freeze(["P0", "P1", "P2", "P3"]);
|
|
27
|
+
|
|
28
|
+
export function toPosix(value) {
|
|
29
|
+
return String(value || "").replace(/\\/g, "/");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function normalizeSeverity(value) {
|
|
33
|
+
const normalized = String(value || "").trim().toUpperCase();
|
|
34
|
+
if (SEVERITIES.includes(normalized)) {
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
return "P2";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createFinding({
|
|
41
|
+
severity,
|
|
42
|
+
kind,
|
|
43
|
+
file,
|
|
44
|
+
line = 0,
|
|
45
|
+
evidence = "",
|
|
46
|
+
rootCause = "",
|
|
47
|
+
recommendedFix = "",
|
|
48
|
+
confidence = null,
|
|
49
|
+
tool = "",
|
|
50
|
+
persona = "observability",
|
|
51
|
+
} = {}) {
|
|
52
|
+
return {
|
|
53
|
+
persona,
|
|
54
|
+
tool: String(tool || "").trim(),
|
|
55
|
+
kind: String(kind || "").trim() || "observability",
|
|
56
|
+
severity: normalizeSeverity(severity),
|
|
57
|
+
file: toPosix(file || ""),
|
|
58
|
+
line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
|
|
59
|
+
evidence: String(evidence || "").trim().slice(0, 400),
|
|
60
|
+
rootCause: String(rootCause || "").trim(),
|
|
61
|
+
recommendedFix: String(recommendedFix || "").trim(),
|
|
62
|
+
confidence:
|
|
63
|
+
confidence === null || confidence === undefined
|
|
64
|
+
? null
|
|
65
|
+
: Math.max(0, Math.min(1, Number(confidence) || 0)),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function readIgnorePatterns(filePath) {
|
|
70
|
+
try {
|
|
71
|
+
const raw = await fsp.readFile(filePath, "utf-8");
|
|
72
|
+
return String(raw || "")
|
|
73
|
+
.split(/\r?\n/)
|
|
74
|
+
.map((line) => line.trim())
|
|
75
|
+
.filter((line) => line && !line.startsWith("#"));
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err && typeof err === "object" && err.code === "ENOENT") {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function createIgnoreMatcher(rootPath) {
|
|
85
|
+
const matcher = ignore();
|
|
86
|
+
const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
|
|
87
|
+
const sentinel = await readIgnorePatterns(
|
|
88
|
+
path.join(rootPath, ".sentinelayerignore")
|
|
89
|
+
);
|
|
90
|
+
matcher.add([...gitignore, ...sentinel]);
|
|
91
|
+
return (relativePath, isDirectory) => {
|
|
92
|
+
const normalized = toPosix(relativePath);
|
|
93
|
+
if (!normalized) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const candidate = isDirectory ? `${normalized}/` : normalized;
|
|
97
|
+
return matcher.ignores(candidate);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function* walkRepoFiles({
|
|
102
|
+
rootPath = process.cwd(),
|
|
103
|
+
extensions = new Set(),
|
|
104
|
+
maxFileSize = MAX_FILE_SIZE_BYTES,
|
|
105
|
+
} = {}) {
|
|
106
|
+
const resolvedRoot = path.resolve(rootPath);
|
|
107
|
+
const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
|
|
108
|
+
const wantedExtensions =
|
|
109
|
+
extensions instanceof Set
|
|
110
|
+
? extensions
|
|
111
|
+
: new Set(Array.isArray(extensions) ? extensions : []);
|
|
112
|
+
const stack = [resolvedRoot];
|
|
113
|
+
while (stack.length > 0) {
|
|
114
|
+
const current = stack.pop();
|
|
115
|
+
let entries = [];
|
|
116
|
+
try {
|
|
117
|
+
entries = await fsp.readdir(current, { withFileTypes: true });
|
|
118
|
+
} catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
const fullPath = path.join(current, entry.name);
|
|
123
|
+
const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
|
|
124
|
+
if (entry.isDirectory()) {
|
|
125
|
+
if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (ignoreMatcher(relativePath, true)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
stack.push(fullPath);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!entry.isFile()) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (ignoreMatcher(relativePath, false)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
141
|
+
if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
let stat = null;
|
|
145
|
+
try {
|
|
146
|
+
stat = await fsp.stat(fullPath);
|
|
147
|
+
} catch {
|
|
148
|
+
stat = null;
|
|
149
|
+
}
|
|
150
|
+
if (!stat || stat.size > maxFileSize) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
yield { fullPath, relativePath };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function findLineMatches(content, pattern) {
|
|
159
|
+
const text = String(content || "");
|
|
160
|
+
if (!pattern) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
const global = new RegExp(
|
|
164
|
+
pattern.source,
|
|
165
|
+
pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`
|
|
166
|
+
);
|
|
167
|
+
const matches = [];
|
|
168
|
+
let match;
|
|
169
|
+
while ((match = global.exec(text)) !== null) {
|
|
170
|
+
const lineIndex = text.slice(0, match.index).split(/\r?\n/).length;
|
|
171
|
+
matches.push({ index: match.index, line: lineIndex, match: match[0] });
|
|
172
|
+
}
|
|
173
|
+
return matches;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function getLineContent(content, line) {
|
|
177
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
178
|
+
return (lines[Math.max(0, (Number(line) || 1) - 1)] || "").trim();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export { DEFAULT_IGNORED_DIRS, MAX_FILE_SIZE_BYTES, SEVERITIES };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// dashboard-gap — check that a dashboard config exists somewhere (#A20).
|
|
2
|
+
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { createFinding, toPosix, walkRepoFiles } from "./base.js";
|
|
7
|
+
|
|
8
|
+
const MANIFEST_SIGNATURES = [
|
|
9
|
+
/(^|\/)dashboards?\//,
|
|
10
|
+
/(^|\/)grafana\//,
|
|
11
|
+
/(^|\/)datadog\//,
|
|
12
|
+
/(^|\/)observability\//,
|
|
13
|
+
];
|
|
14
|
+
const DASHBOARD_FILES = /\.json$|\.yaml$|\.yml$/i;
|
|
15
|
+
|
|
16
|
+
export async function runDashboardGap({ rootPath } = {}) {
|
|
17
|
+
const resolvedRoot = path.resolve(String(rootPath || "."));
|
|
18
|
+
let found = false;
|
|
19
|
+
for await (const { relativePath } of walkRepoFiles({ rootPath: resolvedRoot })) {
|
|
20
|
+
const rel = toPosix(relativePath);
|
|
21
|
+
if (MANIFEST_SIGNATURES.some((p) => p.test(rel)) && DASHBOARD_FILES.test(rel)) {
|
|
22
|
+
found = true;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (found) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
return [
|
|
30
|
+
createFinding({
|
|
31
|
+
tool: "dashboard-gap",
|
|
32
|
+
kind: "observability.no-dashboard",
|
|
33
|
+
severity: "P3",
|
|
34
|
+
file: "",
|
|
35
|
+
line: 0,
|
|
36
|
+
evidence: "No dashboards/, grafana/, datadog/, or observability/ directory with JSON / YAML configs",
|
|
37
|
+
rootCause: "No dashboard config checked into the repo. Ad-hoc dashboards created in the UI are invisible to code review and drift over time.",
|
|
38
|
+
recommendedFix: "Check a machine-readable dashboard source (Grafana JSON, Datadog Terraform, OpenMetrics) into the repo. Generate the deployed dashboard from source to keep them in sync.",
|
|
39
|
+
confidence: 0.55,
|
|
40
|
+
}),
|
|
41
|
+
];
|
|
42
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Sofia (observability persona) domain-tool registry (#A20).
|
|
2
|
+
|
|
3
|
+
import { runAlertAudit } from "./alert-audit.js";
|
|
4
|
+
import { runDashboardGap } from "./dashboard-gap.js";
|
|
5
|
+
import { runLogSchemaCheck } from "./log-schema-check.js";
|
|
6
|
+
import { runSpanCoverage } from "./span-coverage.js";
|
|
7
|
+
|
|
8
|
+
export const OBSERVABILITY_TOOLS = Object.freeze({
|
|
9
|
+
"span-coverage": {
|
|
10
|
+
id: "span-coverage",
|
|
11
|
+
description: "Flag route handlers that have no OpenTelemetry / Sentry / Datadog tracing span in scope.",
|
|
12
|
+
schema: { type: "object", properties: { rootPath: { type: "string" }, files: { type: "array", items: { type: "string" } } } },
|
|
13
|
+
handler: runSpanCoverage,
|
|
14
|
+
},
|
|
15
|
+
"dashboard-gap": {
|
|
16
|
+
id: "dashboard-gap",
|
|
17
|
+
description: "Report if no dashboard configuration (Grafana JSON, Datadog TF, observability dir) is checked into the repo.",
|
|
18
|
+
schema: { type: "object", properties: { rootPath: { type: "string" } } },
|
|
19
|
+
handler: runDashboardGap,
|
|
20
|
+
},
|
|
21
|
+
"alert-audit": {
|
|
22
|
+
id: "alert-audit",
|
|
23
|
+
description: "Report if no declarative alert definitions (Prometheus rules, Datadog monitors, alertmanager) are checked in.",
|
|
24
|
+
schema: { type: "object", properties: { rootPath: { type: "string" } } },
|
|
25
|
+
handler: runAlertAudit,
|
|
26
|
+
},
|
|
27
|
+
"log-schema-check": {
|
|
28
|
+
id: "log-schema-check",
|
|
29
|
+
description: "Flag production source files (not tests / scripts / docs) that use console.log / print() instead of a structured logger.",
|
|
30
|
+
schema: { type: "object", properties: { rootPath: { type: "string" }, files: { type: "array", items: { type: "string" } } } },
|
|
31
|
+
handler: runLogSchemaCheck,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const OBSERVABILITY_TOOL_IDS = Object.freeze(Object.keys(OBSERVABILITY_TOOLS));
|
|
36
|
+
|
|
37
|
+
export async function dispatchObservabilityTool(toolId, args = {}) {
|
|
38
|
+
const tool = OBSERVABILITY_TOOLS[toolId];
|
|
39
|
+
if (!tool) {
|
|
40
|
+
throw new Error(`Unknown observability tool: ${toolId}`);
|
|
41
|
+
}
|
|
42
|
+
return tool.handler(args);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function runAllObservabilityTools({ rootPath, files = null } = {}) {
|
|
46
|
+
const findings = [];
|
|
47
|
+
for (const toolId of OBSERVABILITY_TOOL_IDS) {
|
|
48
|
+
const out = await dispatchObservabilityTool(toolId, { rootPath, files });
|
|
49
|
+
findings.push(...out);
|
|
50
|
+
}
|
|
51
|
+
return findings;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { runAlertAudit, runDashboardGap, runLogSchemaCheck, runSpanCoverage };
|