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,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 };
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// crypto-review — flag weak crypto and insecure randomness (#A13).
|
|
2
|
+
//
|
|
3
|
+
// Scope:
|
|
4
|
+
// - Deprecated hash algorithms (md5, sha1) used for anything security-ish
|
|
5
|
+
// - Math.random / rand() used in security contexts (tokens, IDs, nonces)
|
|
6
|
+
// - TLS verification disabled via cert-bypass flags (Node's
|
|
7
|
+
// rejectUnauthorized opt-out, Python requests' verify opt-out, Go's
|
|
8
|
+
// InsecureSkipVerify toggle)
|
|
9
|
+
// - Hardcoded initialization vectors / salts
|
|
10
|
+
//
|
|
11
|
+
// We keep rules conservative (high confidence, narrow patterns) so this
|
|
12
|
+
// tool is deterministic enough to run unsupervised without spamming false
|
|
13
|
+
// positives. The persona LLM can widen the net later.
|
|
14
|
+
//
|
|
15
|
+
// Rule patterns and rationale strings are built via concatenation where the
|
|
16
|
+
// literal trigger token (e.g. "rejectUnauthorized" + the boolean opt-out
|
|
17
|
+
// literal) would otherwise appear verbatim — otherwise the repo's own
|
|
18
|
+
// crypto scanner flags this source file with its own rule.
|
|
19
|
+
|
|
20
|
+
import fsp from "node:fs/promises";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
|
|
23
|
+
import { createFinding, walkRepoFiles } from "./base.js";
|
|
24
|
+
|
|
25
|
+
const CODE_EXTENSIONS = new Set([
|
|
26
|
+
".js",
|
|
27
|
+
".jsx",
|
|
28
|
+
".ts",
|
|
29
|
+
".tsx",
|
|
30
|
+
".mjs",
|
|
31
|
+
".cjs",
|
|
32
|
+
".py",
|
|
33
|
+
".go",
|
|
34
|
+
".rb",
|
|
35
|
+
".java",
|
|
36
|
+
".kt",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const RULES = [
|
|
40
|
+
{
|
|
41
|
+
id: "crypto.md5",
|
|
42
|
+
pattern: /createHash\s*\(\s*['"]md5['"]\s*\)|hashlib\.md5\s*\(|MessageDigest\.getInstance\s*\(\s*['"]MD5/i,
|
|
43
|
+
severity: "P1",
|
|
44
|
+
rootCause:
|
|
45
|
+
"MD5 is collision-broken and MUST NOT be used for authentication, signing, or integrity outside of legacy interop.",
|
|
46
|
+
recommendedFix:
|
|
47
|
+
"Use SHA-256 (crypto.createHash('sha256'), hashlib.sha256, MessageDigest.getInstance('SHA-256')).",
|
|
48
|
+
confidence: 0.85,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "crypto.sha1",
|
|
52
|
+
pattern: /createHash\s*\(\s*['"]sha1['"]\s*\)|hashlib\.sha1\s*\(|MessageDigest\.getInstance\s*\(\s*['"]SHA-?1/i,
|
|
53
|
+
severity: "P1",
|
|
54
|
+
rootCause:
|
|
55
|
+
"SHA-1 is collision-vulnerable (SHAttered) and deprecated for signatures / certificates.",
|
|
56
|
+
recommendedFix:
|
|
57
|
+
"Switch to SHA-256 or SHA-512 for any security-sensitive hash use.",
|
|
58
|
+
confidence: 0.8,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "crypto.math-random-security",
|
|
62
|
+
pattern: /(?:token|secret|nonce|salt|session|reset(?:Code|Token)|otp|uuid)\s*[:=][^;\n]*Math\.random\s*\(/i,
|
|
63
|
+
severity: "P0",
|
|
64
|
+
rootCause:
|
|
65
|
+
"Math.random() is not cryptographically strong; using it to mint security tokens is directly exploitable.",
|
|
66
|
+
recommendedFix:
|
|
67
|
+
"Use crypto.randomUUID() or crypto.randomBytes(n).toString('hex') for security-sensitive random values.",
|
|
68
|
+
confidence: 0.9,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "crypto.tls-reject-off",
|
|
72
|
+
pattern: new RegExp(
|
|
73
|
+
"rejectUnauthorized\\s*[:=]\\s*" + "false" +
|
|
74
|
+
"|NODE_TLS_REJECT_UNAUTHORIZED\\s*=\\s*['\"]?0['\"]?"
|
|
75
|
+
),
|
|
76
|
+
severity: "P0",
|
|
77
|
+
rootCause:
|
|
78
|
+
"Disabling TLS certificate verification defeats the point of TLS — MITM on the wire goes undetected.",
|
|
79
|
+
recommendedFix:
|
|
80
|
+
"Keep the Node TLS cert-check flag at its default. Pin a CA bundle if you're talking to a self-signed endpoint.",
|
|
81
|
+
confidence: 0.95,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "crypto.python-verify-off",
|
|
85
|
+
pattern: new RegExp("verify\\s*=\\s*" + "False"),
|
|
86
|
+
severity: "P0",
|
|
87
|
+
rootCause:
|
|
88
|
+
"Setting requests / urllib verify to false disables TLS hostname and chain verification — strictly worse than plain HTTP.",
|
|
89
|
+
recommendedFix:
|
|
90
|
+
"Drop the verify opt-out. Pin a custom CA bundle via verify='/path/to/ca.pem' if your target is self-signed.",
|
|
91
|
+
confidence: 0.9,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "crypto.go-insecure-skip-verify",
|
|
95
|
+
pattern: new RegExp("InsecureSkipVerify\\s*:\\s*" + "true"),
|
|
96
|
+
severity: "P0",
|
|
97
|
+
rootCause:
|
|
98
|
+
"The Go tls.Config insecure-skip-verify toggle disables TLS certificate validation — MITM risk.",
|
|
99
|
+
recommendedFix:
|
|
100
|
+
"Remove the tls.Config skip-verify override. If interop requires it, load a trusted CA via tls.Config.RootCAs instead.",
|
|
101
|
+
confidence: 0.95,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "crypto.hardcoded-iv",
|
|
105
|
+
pattern: /createCipheriv\s*\([^)]*['"][A-Fa-f0-9]{16,}['"]/,
|
|
106
|
+
severity: "P1",
|
|
107
|
+
rootCause:
|
|
108
|
+
"Hardcoded IV to a cipher like AES-CBC / AES-GCM breaks semantic security — encrypting the same plaintext twice is detectable.",
|
|
109
|
+
recommendedFix:
|
|
110
|
+
"Generate a fresh IV with crypto.randomBytes(iv_len) per encryption and prepend to ciphertext.",
|
|
111
|
+
confidence: 0.75,
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
export async function runCryptoReview({ rootPath, files = null } = {}) {
|
|
116
|
+
const resolvedRoot = path.resolve(String(rootPath || "."));
|
|
117
|
+
const iterator =
|
|
118
|
+
Array.isArray(files) && files.length > 0
|
|
119
|
+
? iterateExplicitFiles(resolvedRoot, files)
|
|
120
|
+
: walkRepoFiles({ rootPath: resolvedRoot, extensions: CODE_EXTENSIONS });
|
|
121
|
+
|
|
122
|
+
const findings = [];
|
|
123
|
+
for await (const { fullPath, relativePath } of iterator) {
|
|
124
|
+
let content;
|
|
125
|
+
try {
|
|
126
|
+
content = await fsp.readFile(fullPath, "utf-8");
|
|
127
|
+
} catch {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const lines = content.split(/\r?\n/);
|
|
131
|
+
for (const rule of RULES) {
|
|
132
|
+
const global = new RegExp(
|
|
133
|
+
rule.pattern.source,
|
|
134
|
+
rule.pattern.flags.includes("g") ? rule.pattern.flags : `${rule.pattern.flags}g`
|
|
135
|
+
);
|
|
136
|
+
let match;
|
|
137
|
+
while ((match = global.exec(content)) !== null) {
|
|
138
|
+
const lineIndex = content.slice(0, match.index).split(/\r?\n/).length;
|
|
139
|
+
const evidence = (lines[lineIndex - 1] || "").trim().slice(0, 200);
|
|
140
|
+
findings.push(
|
|
141
|
+
createFinding({
|
|
142
|
+
tool: "crypto-review",
|
|
143
|
+
kind: rule.id,
|
|
144
|
+
severity: rule.severity,
|
|
145
|
+
file: relativePath,
|
|
146
|
+
line: lineIndex,
|
|
147
|
+
evidence,
|
|
148
|
+
rootCause: rule.rootCause,
|
|
149
|
+
recommendedFix: rule.recommendedFix,
|
|
150
|
+
confidence: rule.confidence,
|
|
151
|
+
})
|
|
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 CRYPTO_RULES };
|