codegate-ai 0.7.0 → 0.9.0
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 +109 -40
- package/dist/cli.js +85 -7
- package/dist/commands/scan-command.d.ts +6 -1
- package/dist/commands/scan-command.js +1 -1
- package/dist/commands/scan-content-command.d.ts +16 -0
- package/dist/commands/scan-content-command.js +65 -0
- package/dist/config/inline-ignore.d.ts +11 -0
- package/dist/config/inline-ignore.js +57 -0
- package/dist/config/suppression-policy.d.ts +22 -0
- package/dist/config/suppression-policy.js +146 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.js +188 -3
- package/dist/layer2-static/action/parser.d.ts +3 -0
- package/dist/layer2-static/action/parser.js +165 -0
- package/dist/layer2-static/action/types.d.ts +39 -0
- package/dist/layer2-static/action/types.js +1 -0
- package/dist/layer2-static/advisories/agent-components.json +62 -0
- package/dist/layer2-static/advisories/cache.d.ts +6 -0
- package/dist/layer2-static/advisories/cache.js +44 -0
- package/dist/layer2-static/advisories/cache.ts +62 -0
- package/dist/layer2-static/advisories/gha-advisory-client.d.ts +7 -0
- package/dist/layer2-static/advisories/gha-advisory-client.js +4 -0
- package/dist/layer2-static/advisories/gha-advisory-client.ts +13 -0
- package/dist/layer2-static/advisories/gha-known-vulnerable-actions.json +4 -0
- package/dist/layer2-static/audits/registry.d.ts +3 -0
- package/dist/layer2-static/audits/registry.js +29 -0
- package/dist/layer2-static/audits/types.d.ts +12 -0
- package/dist/layer2-static/audits/types.js +1 -0
- package/dist/layer2-static/dependabot/parser.d.ts +3 -0
- package/dist/layer2-static/dependabot/parser.js +189 -0
- package/dist/layer2-static/dependabot/types.d.ts +50 -0
- package/dist/layer2-static/dependabot/types.js +1 -0
- package/dist/layer2-static/detectors/advisory-intelligence.d.ts +7 -0
- package/dist/layer2-static/detectors/advisory-intelligence.js +170 -0
- package/dist/layer2-static/detectors/command-exec.js +6 -0
- package/dist/layer2-static/detectors/dependabot-cooldown.d.ts +7 -0
- package/dist/layer2-static/detectors/dependabot-cooldown.js +54 -0
- package/dist/layer2-static/detectors/dependabot-execution.d.ts +7 -0
- package/dist/layer2-static/detectors/dependabot-execution.js +68 -0
- package/dist/layer2-static/detectors/rule-file.js +5 -0
- package/dist/layer2-static/detectors/workflow-anonymous-definition.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-anonymous-definition.js +48 -0
- package/dist/layer2-static/detectors/workflow-archived-uses.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-archived-uses.js +147 -0
- package/dist/layer2-static/detectors/workflow-artipacked.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-artipacked.js +62 -0
- package/dist/layer2-static/detectors/workflow-bot-conditions.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-bot-conditions.js +108 -0
- package/dist/layer2-static/detectors/workflow-cache-poisoning.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-cache-poisoning.js +67 -0
- package/dist/layer2-static/detectors/workflow-concurrency-limits.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-concurrency-limits.js +76 -0
- package/dist/layer2-static/detectors/workflow-dangerous-triggers.d.ts +6 -0
- package/dist/layer2-static/detectors/workflow-dangerous-triggers.js +38 -0
- package/dist/layer2-static/detectors/workflow-excessive-permissions.d.ts +6 -0
- package/dist/layer2-static/detectors/workflow-excessive-permissions.js +65 -0
- package/dist/layer2-static/detectors/workflow-forbidden-uses.d.ts +12 -0
- package/dist/layer2-static/detectors/workflow-forbidden-uses.js +148 -0
- package/dist/layer2-static/detectors/workflow-github-env.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-github-env.js +49 -0
- package/dist/layer2-static/detectors/workflow-hardcoded-container-credentials.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-hardcoded-container-credentials.js +90 -0
- package/dist/layer2-static/detectors/workflow-impostor-commit.d.ts +9 -0
- package/dist/layer2-static/detectors/workflow-impostor-commit.js +127 -0
- package/dist/layer2-static/detectors/workflow-insecure-commands.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-insecure-commands.js +72 -0
- package/dist/layer2-static/detectors/workflow-known-vuln-action.d.ts +8 -0
- package/dist/layer2-static/detectors/workflow-known-vuln-action.js +70 -0
- package/dist/layer2-static/detectors/workflow-misfeature.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-misfeature.js +101 -0
- package/dist/layer2-static/detectors/workflow-obfuscation.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-obfuscation.js +79 -0
- package/dist/layer2-static/detectors/workflow-overprovisioned-secrets.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-overprovisioned-secrets.js +81 -0
- package/dist/layer2-static/detectors/workflow-ref-confusion.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-ref-confusion.js +69 -0
- package/dist/layer2-static/detectors/workflow-ref-version-mismatch.d.ts +9 -0
- package/dist/layer2-static/detectors/workflow-ref-version-mismatch.js +179 -0
- package/dist/layer2-static/detectors/workflow-secrets-inherit.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-secrets-inherit.js +59 -0
- package/dist/layer2-static/detectors/workflow-secrets-outside-env.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-secrets-outside-env.js +99 -0
- package/dist/layer2-static/detectors/workflow-self-hosted-runner.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-self-hosted-runner.js +67 -0
- package/dist/layer2-static/detectors/workflow-stale-action-refs.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-stale-action-refs.js +178 -0
- package/dist/layer2-static/detectors/workflow-superfluous-actions.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-superfluous-actions.js +70 -0
- package/dist/layer2-static/detectors/workflow-template-injection.d.ts +6 -0
- package/dist/layer2-static/detectors/workflow-template-injection.js +94 -0
- package/dist/layer2-static/detectors/workflow-undocumented-permissions.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-undocumented-permissions.js +101 -0
- package/dist/layer2-static/detectors/workflow-unpinned-images.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-unpinned-images.js +108 -0
- package/dist/layer2-static/detectors/workflow-unpinned-uses.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-unpinned-uses.js +60 -0
- package/dist/layer2-static/detectors/workflow-unredacted-secrets.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-unredacted-secrets.js +92 -0
- package/dist/layer2-static/detectors/workflow-unsound-condition.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-unsound-condition.js +90 -0
- package/dist/layer2-static/detectors/workflow-unsound-contains.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-unsound-contains.js +88 -0
- package/dist/layer2-static/detectors/workflow-use-trusted-publishing.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-use-trusted-publishing.js +66 -0
- package/dist/layer2-static/engine.d.ts +13 -2
- package/dist/layer2-static/engine.js +578 -41
- package/dist/layer2-static/github/cache.d.ts +8 -0
- package/dist/layer2-static/github/cache.js +41 -0
- package/dist/layer2-static/github/client.d.ts +17 -0
- package/dist/layer2-static/github/client.js +43 -0
- package/dist/layer2-static/rule-engine.d.ts +1 -1
- package/dist/layer2-static/rule-engine.js +1 -13
- package/dist/layer2-static/rule-pack-loader.d.ts +10 -0
- package/dist/layer2-static/rule-pack-loader.js +187 -0
- package/dist/layer2-static/workflow/injection-sinks.json +5 -0
- package/dist/layer2-static/workflow/parser.d.ts +3 -0
- package/dist/layer2-static/workflow/parser.js +88 -0
- package/dist/layer2-static/workflow/types.d.ts +15 -0
- package/dist/layer2-static/workflow/types.js +1 -0
- package/dist/layer3-dynamic/toxic-flow.js +6 -0
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +11 -10
- package/dist/report/finding-fingerprint.d.ts +5 -0
- package/dist/report/finding-fingerprint.js +47 -0
- package/dist/reporter/markdown.js +25 -3
- package/dist/reporter/sarif.js +25 -5
- package/dist/reporter/terminal.js +37 -0
- package/dist/scan-target/fetch-plan.d.ts +8 -0
- package/dist/scan-target/fetch-plan.js +30 -0
- package/dist/scan-target/staging.js +60 -5
- package/dist/scan.d.ts +7 -1
- package/dist/scan.js +157 -40
- package/dist/types/finding.d.ts +10 -1
- package/dist/types/finding.js +5 -0
- package/package.json +1 -1
|
@@ -15,6 +15,27 @@ function appendLabeledText(lines, label, value) {
|
|
|
15
15
|
}
|
|
16
16
|
lines.push(` ${label}: ${value}`);
|
|
17
17
|
}
|
|
18
|
+
function appendMetadata(lines, metadata) {
|
|
19
|
+
if (!metadata) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const hasContent = (metadata.sources?.length ?? 0) > 0 ||
|
|
23
|
+
(metadata.sinks?.length ?? 0) > 0 ||
|
|
24
|
+
(metadata.referenced_secrets?.length ?? 0) > 0 ||
|
|
25
|
+
(metadata.risk_tags?.length ?? 0) > 0 ||
|
|
26
|
+
typeof metadata.origin === "string";
|
|
27
|
+
if (!hasContent) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
lines.push(" Metadata:");
|
|
31
|
+
appendLabeledList(lines, "Sources", metadata.sources ?? []);
|
|
32
|
+
appendLabeledList(lines, "Sinks", metadata.sinks ?? []);
|
|
33
|
+
appendLabeledList(lines, "Referenced secrets", metadata.referenced_secrets ?? []);
|
|
34
|
+
appendLabeledList(lines, "Risk tags", metadata.risk_tags ?? []);
|
|
35
|
+
if (metadata.origin) {
|
|
36
|
+
appendLabeledText(lines, "Origin", metadata.origin);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
18
39
|
function appendEvidence(lines, evidence) {
|
|
19
40
|
const evidenceLines = evidence.split("\n");
|
|
20
41
|
if (evidenceLines.length === 1) {
|
|
@@ -55,11 +76,26 @@ function appendFinding(lines, report, options, finding) {
|
|
|
55
76
|
if (verbose) {
|
|
56
77
|
lines.push(` Rule: ${finding.rule_id}`);
|
|
57
78
|
lines.push(` Finding ID: ${finding.finding_id}`);
|
|
79
|
+
if (finding.fingerprint) {
|
|
80
|
+
lines.push(` Fingerprint: ${finding.fingerprint}`);
|
|
81
|
+
}
|
|
58
82
|
lines.push(` Category: ${finding.category} | Layer: ${finding.layer} | Confidence: ${finding.confidence}`);
|
|
59
83
|
const formattedLocation = formatLocation(finding.location);
|
|
60
84
|
if (formattedLocation) {
|
|
61
85
|
lines.push(` Location: ${formattedLocation}`);
|
|
62
86
|
}
|
|
87
|
+
if ((finding.affected_locations?.length ?? 0) > 0) {
|
|
88
|
+
lines.push(" Affected locations:");
|
|
89
|
+
for (const location of finding.affected_locations ?? []) {
|
|
90
|
+
const path = toAbsoluteDisplayPath(report.scan_target, location.file_path);
|
|
91
|
+
const locationText = formatLocation({
|
|
92
|
+
field: location.location?.field,
|
|
93
|
+
line: location.location?.line,
|
|
94
|
+
column: location.location?.column,
|
|
95
|
+
});
|
|
96
|
+
lines.push(` - ${path}${locationText ? ` (${locationText})` : ""}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
63
99
|
if (finding.cve) {
|
|
64
100
|
lines.push(` CVE: ${finding.cve}`);
|
|
65
101
|
}
|
|
@@ -70,6 +106,7 @@ function appendFinding(lines, report, options, finding) {
|
|
|
70
106
|
if (finding.remediation_actions.length > 0) {
|
|
71
107
|
lines.push(` Remediation: ${finding.remediation_actions.join(", ")}`);
|
|
72
108
|
}
|
|
109
|
+
appendMetadata(lines, finding.metadata);
|
|
73
110
|
}
|
|
74
111
|
if (finding.layer === "L3" && finding.source_config) {
|
|
75
112
|
const fieldSuffix = finding.source_config.field ? ` (${finding.source_config.field})` : "";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface SparseFetchPlan {
|
|
2
|
+
sparsePaths: string[];
|
|
3
|
+
}
|
|
4
|
+
export interface BuildSparseFetchPlanInput {
|
|
5
|
+
preferredSkill?: string;
|
|
6
|
+
inferredSkill?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function buildSparseFetchPlan(source: string, input?: BuildSparseFetchPlanInput): SparseFetchPlan | null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { isLikelyGitRepoUrl } from "./helpers.js";
|
|
2
|
+
function normalizeSkillName(value) {
|
|
3
|
+
const trimmed = value?.trim();
|
|
4
|
+
if (!trimmed) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
return trimmed.replace(/^skills\//iu, "").replace(/^\/+/u, "");
|
|
8
|
+
}
|
|
9
|
+
function selectSkill(input) {
|
|
10
|
+
return normalizeSkillName(input.preferredSkill) ?? normalizeSkillName(input.inferredSkill);
|
|
11
|
+
}
|
|
12
|
+
export function buildSparseFetchPlan(source, input = {}) {
|
|
13
|
+
let url;
|
|
14
|
+
try {
|
|
15
|
+
url = new URL(source);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
if (!isLikelyGitRepoUrl(url)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const selectedSkill = selectSkill(input);
|
|
24
|
+
if (!selectedSkill) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
sparsePaths: ["/*", "!/*/", "/.*/**", "/hooks/**", `/skills/${selectedSkill}/**`],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -2,16 +2,71 @@ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, statSync
|
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
+
import { buildSparseFetchPlan } from "./fetch-plan.js";
|
|
5
6
|
import { cleanupTempDir, collectExplicitCandidates, copyDirectoryRecursive, extractSkillFromRepoPath, inferRemoteCandidateFormat, inferLocalFileStagePath, inferRemoteFileStagePath, inferToolFromReportPath, parseGitHubFileSource, preserveTailSegments, shouldStageContainingFolder, } from "./helpers.js";
|
|
6
|
-
function
|
|
7
|
-
const result = spawnSync("git",
|
|
7
|
+
function runGit(args) {
|
|
8
|
+
const result = spawnSync("git", args, {
|
|
9
|
+
encoding: "utf8",
|
|
10
|
+
});
|
|
11
|
+
if (result.status !== 0) {
|
|
12
|
+
const stderr = result.stderr?.trim();
|
|
13
|
+
throw new Error(stderr && stderr.length > 0 ? stderr : `git ${args.join(" ")} failed`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function cloneRepository(source, destination, sparsePaths) {
|
|
17
|
+
const cloneArgs = ["clone", "--depth", "1", "--filter=blob:none"];
|
|
18
|
+
if (sparsePaths && sparsePaths.length > 0) {
|
|
19
|
+
cloneArgs.push("--no-checkout");
|
|
20
|
+
}
|
|
21
|
+
cloneArgs.push(source, destination);
|
|
22
|
+
const result = spawnSync("git", cloneArgs, {
|
|
8
23
|
encoding: "utf8",
|
|
9
24
|
});
|
|
10
25
|
if (result.status !== 0) {
|
|
11
26
|
const stderr = result.stderr?.trim();
|
|
12
27
|
throw new Error(stderr && stderr.length > 0 ? stderr : `git clone failed for ${source}`);
|
|
13
28
|
}
|
|
14
|
-
|
|
29
|
+
if (!sparsePaths || sparsePaths.length === 0) {
|
|
30
|
+
cleanupTempDir(join(destination, ".git"));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
runGit(["-C", destination, "sparse-checkout", "init", "--no-cone"]);
|
|
35
|
+
runGit(["-C", destination, "sparse-checkout", "set", "--no-cone", ...sparsePaths]);
|
|
36
|
+
runGit(["-C", destination, "checkout", "--force", "HEAD"]);
|
|
37
|
+
cleanupTempDir(join(destination, ".git"));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
cleanupTempDir(destination);
|
|
42
|
+
const fallback = spawnSync("git", ["clone", "--depth", "1", "--filter=blob:none", source, destination], {
|
|
43
|
+
encoding: "utf8",
|
|
44
|
+
});
|
|
45
|
+
if (fallback.status !== 0) {
|
|
46
|
+
const stderr = fallback.stderr?.trim();
|
|
47
|
+
throw new Error(stderr && stderr.length > 0 ? stderr : `git clone failed for ${source}`, {
|
|
48
|
+
cause: error,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
cleanupTempDir(join(destination, ".git"));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function cloneSparseRepository(source, destination, sparsePaths) {
|
|
55
|
+
cloneRepository(source, destination, sparsePaths);
|
|
56
|
+
}
|
|
57
|
+
function cloneFullRepository(source, destination) {
|
|
58
|
+
cloneRepository(source, destination);
|
|
59
|
+
}
|
|
60
|
+
function stageSparseClone(source, destination, preferredSkill, inferredSkill) {
|
|
61
|
+
const sparsePlan = buildSparseFetchPlan(source, {
|
|
62
|
+
preferredSkill,
|
|
63
|
+
inferredSkill,
|
|
64
|
+
});
|
|
65
|
+
if (!sparsePlan) {
|
|
66
|
+
cloneFullRepository(source, destination);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
cloneSparseRepository(source, destination, sparsePlan.sparsePaths);
|
|
15
70
|
}
|
|
16
71
|
export function stageLocalFile(absolutePath) {
|
|
17
72
|
const tempRoot = mkdtempSync(join(tmpdir(), "codegate-scan-target-"));
|
|
@@ -132,7 +187,7 @@ export async function cloneGitRepo(rawTarget, options = {}) {
|
|
|
132
187
|
const tempRoot = mkdtempSync(join(tmpdir(), "codegate-scan-repo-"));
|
|
133
188
|
const repoDir = join(tempRoot, "repo");
|
|
134
189
|
try {
|
|
135
|
-
|
|
190
|
+
stageSparseClone(rawTarget, repoDir, options.preferredSkill, options.inferredSkill);
|
|
136
191
|
return await stageSkillAwareRepository(tempRoot, repoDir, options.displayTarget ?? rawTarget, options);
|
|
137
192
|
}
|
|
138
193
|
catch (error) {
|
|
@@ -144,7 +199,7 @@ export function stageRepoSubdirectory(repoUrl, filePath, displayTarget) {
|
|
|
144
199
|
const tempRoot = mkdtempSync(join(tmpdir(), "codegate-scan-repo-file-"));
|
|
145
200
|
const repoDir = join(tempRoot, "repo");
|
|
146
201
|
try {
|
|
147
|
-
|
|
202
|
+
stageSparseClone(repoUrl, repoDir, undefined, extractSkillFromRepoPath(filePath) ?? undefined);
|
|
148
203
|
const inferredSkill = extractSkillFromRepoPath(filePath);
|
|
149
204
|
if (inferredSkill) {
|
|
150
205
|
const stageRoot = join(tempRoot, "staged");
|
package/dist/scan.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { type KnowledgeBaseLoadResult } from "./layer1-discovery/knowledge-base.
|
|
|
4
4
|
import { type WalkResult } from "./layer1-discovery/file-walker.js";
|
|
5
5
|
import type { DiscoveryFormat } from "./types/discovery.js";
|
|
6
6
|
import type { CodeGateReport } from "./types/report.js";
|
|
7
|
-
import type { CodeGateConfig } from "./config.js";
|
|
7
|
+
import type { CodeGateConfig, ScanCollectionKind, ScanCollectionMode } from "./config.js";
|
|
8
8
|
import type { DeepScanResource } from "./pipeline.js";
|
|
9
9
|
export interface ScanEngineInput {
|
|
10
10
|
version: string;
|
|
@@ -18,10 +18,14 @@ export interface ScanEngineInput {
|
|
|
18
18
|
export interface DeepScanDiscoveryOptions {
|
|
19
19
|
includeUserScope?: boolean;
|
|
20
20
|
homeDir?: string;
|
|
21
|
+
collectModes?: ScanCollectionMode[];
|
|
22
|
+
collectKinds?: ScanCollectionKind[];
|
|
21
23
|
}
|
|
22
24
|
export interface ScanSurfaceOptions {
|
|
23
25
|
includeUserScope?: boolean;
|
|
24
26
|
homeDir?: string;
|
|
27
|
+
collectModes?: ScanCollectionMode[];
|
|
28
|
+
collectKinds?: ScanCollectionKind[];
|
|
25
29
|
}
|
|
26
30
|
export interface ScanDiscoveryCandidate {
|
|
27
31
|
reportPath: string;
|
|
@@ -45,6 +49,8 @@ export interface ScanDiscoveryContextOptions {
|
|
|
45
49
|
homeDir?: string;
|
|
46
50
|
parseSelected?: boolean;
|
|
47
51
|
explicitCandidates?: ScanDiscoveryCandidate[];
|
|
52
|
+
collectModes?: ScanCollectionMode[];
|
|
53
|
+
collectKinds?: ScanCollectionKind[];
|
|
48
54
|
}
|
|
49
55
|
export declare function discoverDeepScanResources(scanTarget: string, kbInput?: KnowledgeBaseLoadResult, options?: DeepScanDiscoveryOptions): DeepScanResource[];
|
|
50
56
|
export declare function createScanDiscoveryContext(scanTarget: string, kbInput?: KnowledgeBaseLoadResult, options?: ScanDiscoveryContextOptions): ScanDiscoveryContext;
|
package/dist/scan.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { basename, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { collectLocalTextAnalysisTargets, } from "./layer3-dynamic/local-text-analysis.js";
|
|
5
5
|
import { runStaticPipeline } from "./pipeline.js";
|
|
6
6
|
import { applyReportSummary } from "./report-summary.js";
|
|
@@ -9,11 +9,28 @@ import { loadKnowledgeBase, } from "./layer1-discovery/knowledge-base.js";
|
|
|
9
9
|
import { detectTools } from "./layer1-discovery/tool-detector.js";
|
|
10
10
|
import { walkProjectTree } from "./layer1-discovery/file-walker.js";
|
|
11
11
|
import { evaluateScanStateSnapshots, extractMcpServerSnapshots, loadScanState, saveScanState, } from "./layer2-static/state/scan-state.js";
|
|
12
|
+
import { applyInlineIgnoreDirectives, collectInlineIgnoreDirectives, } from "./config/inline-ignore.js";
|
|
13
|
+
import { isGitHubDependabotPath } from "./layer2-static/dependabot/parser.js";
|
|
12
14
|
const MCP_SERVER_CONTAINER_KEYS = ["mcpServers", "mcp_servers", "context_servers"];
|
|
13
15
|
const REMOTE_MCP_SERVER_ARRAY_KEYS = ["remoteMCPServers", "remote_mcp_servers"];
|
|
14
16
|
const USER_SCOPE_WILDCARD_MAX_DEPTH = 6;
|
|
15
17
|
const USER_SCOPE_WILDCARD_MAX_FILES = 500;
|
|
16
18
|
const INFERRED_ARTIFACT_RULES = [
|
|
19
|
+
{
|
|
20
|
+
pattern: /(?:^|\/)\.github\/workflows\/[^/]+\.ya?ml$/iu,
|
|
21
|
+
format: "yaml",
|
|
22
|
+
tool: "github-actions",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
pattern: /(?:^|\/)\.github\/dependabot\.ya?ml$/iu,
|
|
26
|
+
format: "yaml",
|
|
27
|
+
tool: "dependabot",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
pattern: /(?:^|\/)action\.ya?ml$/iu,
|
|
31
|
+
format: "yaml",
|
|
32
|
+
tool: "github-actions",
|
|
33
|
+
},
|
|
17
34
|
{ pattern: /(?:^|\/)agents\.md$/iu, format: "markdown", tool: "claude-code" },
|
|
18
35
|
{ pattern: /(?:^|\/)claude\.md$/iu, format: "markdown", tool: "claude-code" },
|
|
19
36
|
{ pattern: /(?:^|\/)codex\.md$/iu, format: "markdown", tool: "codex-cli" },
|
|
@@ -37,6 +54,12 @@ function wildcardToRegex(pattern) {
|
|
|
37
54
|
function normalizePathForMatch(path) {
|
|
38
55
|
return path.split(sep).join("/");
|
|
39
56
|
}
|
|
57
|
+
function normalizeCollectionKinds(input) {
|
|
58
|
+
if (!input || input.length === 0) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return new Set(input);
|
|
62
|
+
}
|
|
40
63
|
function normalizeUserScopePattern(pattern) {
|
|
41
64
|
return normalizePathForMatch(pattern).replace(/^~\//u, "").replace(/^\/+/u, "");
|
|
42
65
|
}
|
|
@@ -63,6 +86,32 @@ function gatherCandidatePatterns(kb) {
|
|
|
63
86
|
}
|
|
64
87
|
return candidates;
|
|
65
88
|
}
|
|
89
|
+
function isWorkflowCollectionCandidate(reportPath) {
|
|
90
|
+
return /(?:^|\/)\.github\/workflows\/[^/]+\.ya?ml$/iu.test(normalizePathForMatch(reportPath));
|
|
91
|
+
}
|
|
92
|
+
function isActionCollectionCandidate(reportPath) {
|
|
93
|
+
const fileName = basename(normalizePathForMatch(reportPath)).toLowerCase();
|
|
94
|
+
return fileName === "action.yml" || fileName === "action.yaml";
|
|
95
|
+
}
|
|
96
|
+
function inferCollectionKind(reportPath) {
|
|
97
|
+
if (isWorkflowCollectionCandidate(reportPath)) {
|
|
98
|
+
return "workflows";
|
|
99
|
+
}
|
|
100
|
+
if (isActionCollectionCandidate(reportPath)) {
|
|
101
|
+
return "actions";
|
|
102
|
+
}
|
|
103
|
+
if (isGitHubDependabotPath(reportPath)) {
|
|
104
|
+
return "dependabot";
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
function matchesCollectionKinds(reportPath, collectKinds) {
|
|
109
|
+
if (!collectKinds) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
const kind = inferCollectionKind(reportPath);
|
|
113
|
+
return kind !== null && collectKinds.has(kind);
|
|
114
|
+
}
|
|
66
115
|
function isRegularFile(path) {
|
|
67
116
|
try {
|
|
68
117
|
return statSync(path).isFile();
|
|
@@ -149,6 +198,11 @@ function collectUserScopeWildcardMatches(homeDir, pattern) {
|
|
|
149
198
|
}
|
|
150
199
|
function collectSelectedCandidates(absoluteTarget, walkedFiles, patterns, options) {
|
|
151
200
|
const selected = new Map();
|
|
201
|
+
const includeAll = options.collectModes.has("all");
|
|
202
|
+
const includeProject = includeAll || options.collectModes.has("default") || options.collectModes.has("project");
|
|
203
|
+
const includeUser = includeAll ||
|
|
204
|
+
options.collectModes.has("user") ||
|
|
205
|
+
(options.collectModes.has("default") && options.includeUserScope);
|
|
152
206
|
const filesByRelativePath = walkedFiles
|
|
153
207
|
.map((filePath) => ({
|
|
154
208
|
absolutePath: filePath,
|
|
@@ -156,6 +210,9 @@ function collectSelectedCandidates(absoluteTarget, walkedFiles, patterns, option
|
|
|
156
210
|
}))
|
|
157
211
|
.filter((entry) => !entry.relativePath.startsWith(".."));
|
|
158
212
|
for (const file of filesByRelativePath) {
|
|
213
|
+
if (!includeProject) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
159
216
|
for (const candidate of patterns) {
|
|
160
217
|
if (candidate.scope !== "project") {
|
|
161
218
|
continue;
|
|
@@ -163,6 +220,9 @@ function collectSelectedCandidates(absoluteTarget, walkedFiles, patterns, option
|
|
|
163
220
|
if (!wildcardToRegex(candidate.pattern).test(file.relativePath)) {
|
|
164
221
|
continue;
|
|
165
222
|
}
|
|
223
|
+
if (!matchesCollectionKinds(file.relativePath, options.collectKinds)) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
166
226
|
if (!selected.has(file.relativePath)) {
|
|
167
227
|
selected.set(file.relativePath, {
|
|
168
228
|
reportPath: file.relativePath,
|
|
@@ -173,8 +233,14 @@ function collectSelectedCandidates(absoluteTarget, walkedFiles, patterns, option
|
|
|
173
233
|
}
|
|
174
234
|
}
|
|
175
235
|
}
|
|
176
|
-
if (!
|
|
236
|
+
if (!includeUser) {
|
|
177
237
|
for (const file of filesByRelativePath) {
|
|
238
|
+
if (!includeProject) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (!matchesCollectionKinds(file.relativePath, options.collectKinds)) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
178
244
|
if (selected.has(file.relativePath)) {
|
|
179
245
|
continue;
|
|
180
246
|
}
|
|
@@ -186,40 +252,54 @@ function collectSelectedCandidates(absoluteTarget, walkedFiles, patterns, option
|
|
|
186
252
|
}
|
|
187
253
|
return Array.from(selected.values());
|
|
188
254
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
255
|
+
if (includeUser) {
|
|
256
|
+
for (const candidate of patterns) {
|
|
257
|
+
if (candidate.scope !== "user") {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const userPattern = normalizeUserScopePattern(candidate.pattern);
|
|
261
|
+
if (userPattern.includes("*")) {
|
|
262
|
+
for (const match of collectUserScopeWildcardMatches(options.homeDir, userPattern)) {
|
|
263
|
+
const reportPath = toUserReportPath(match.relativePath);
|
|
264
|
+
if (!matchesCollectionKinds(reportPath, options.collectKinds)) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (!selected.has(reportPath)) {
|
|
268
|
+
selected.set(reportPath, {
|
|
269
|
+
reportPath,
|
|
270
|
+
absolutePath: match.absolutePath,
|
|
271
|
+
format: candidate.format,
|
|
272
|
+
tool: candidate.tool,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
204
275
|
}
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const absolutePath = resolve(options.homeDir, userPattern);
|
|
279
|
+
if (!existsSync(absolutePath) || !isRegularFile(absolutePath)) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const reportPath = toUserReportPath(userPattern);
|
|
283
|
+
if (!matchesCollectionKinds(reportPath, options.collectKinds)) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (!selected.has(reportPath)) {
|
|
287
|
+
selected.set(reportPath, {
|
|
288
|
+
reportPath,
|
|
289
|
+
absolutePath,
|
|
290
|
+
format: candidate.format,
|
|
291
|
+
tool: candidate.tool,
|
|
292
|
+
});
|
|
205
293
|
}
|
|
206
|
-
continue;
|
|
207
294
|
}
|
|
208
|
-
|
|
209
|
-
|
|
295
|
+
}
|
|
296
|
+
for (const file of filesByRelativePath) {
|
|
297
|
+
if (!includeProject && !includeAll) {
|
|
210
298
|
continue;
|
|
211
299
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
selected.set(reportPath, {
|
|
215
|
-
reportPath,
|
|
216
|
-
absolutePath,
|
|
217
|
-
format: candidate.format,
|
|
218
|
-
tool: candidate.tool,
|
|
219
|
-
});
|
|
300
|
+
if (!matchesCollectionKinds(file.relativePath, options.collectKinds)) {
|
|
301
|
+
continue;
|
|
220
302
|
}
|
|
221
|
-
}
|
|
222
|
-
for (const file of filesByRelativePath) {
|
|
223
303
|
if (selected.has(file.relativePath)) {
|
|
224
304
|
continue;
|
|
225
305
|
}
|
|
@@ -231,7 +311,17 @@ function collectSelectedCandidates(absoluteTarget, walkedFiles, patterns, option
|
|
|
231
311
|
}
|
|
232
312
|
return Array.from(selected.values());
|
|
233
313
|
}
|
|
234
|
-
function
|
|
314
|
+
function normalizeCollectionModes(input) {
|
|
315
|
+
const normalized = new Set();
|
|
316
|
+
for (const mode of input ?? []) {
|
|
317
|
+
normalized.add(mode);
|
|
318
|
+
}
|
|
319
|
+
if (normalized.size === 0) {
|
|
320
|
+
normalized.add("default");
|
|
321
|
+
}
|
|
322
|
+
return normalized;
|
|
323
|
+
}
|
|
324
|
+
function mergeExplicitCandidates(selected, explicitCandidates, collectKinds) {
|
|
235
325
|
if (!explicitCandidates || explicitCandidates.length === 0) {
|
|
236
326
|
return selected;
|
|
237
327
|
}
|
|
@@ -240,6 +330,9 @@ function mergeExplicitCandidates(selected, explicitCandidates) {
|
|
|
240
330
|
merged.set(candidate.reportPath, candidate);
|
|
241
331
|
}
|
|
242
332
|
for (const candidate of explicitCandidates) {
|
|
333
|
+
if (!matchesCollectionKinds(candidate.reportPath, collectKinds)) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
243
336
|
merged.set(candidate.reportPath, candidate);
|
|
244
337
|
}
|
|
245
338
|
return Array.from(merged.values());
|
|
@@ -272,11 +365,11 @@ function ensureParsedCandidates(context) {
|
|
|
272
365
|
}
|
|
273
366
|
return context.parsedCandidates;
|
|
274
367
|
}
|
|
275
|
-
function makeParseErrorFinding(filePath, tool, message) {
|
|
368
|
+
function makeParseErrorFinding(filePath, tool, message, strictCollection) {
|
|
276
369
|
return {
|
|
277
370
|
rule_id: "parse-error",
|
|
278
371
|
finding_id: `PARSE_ERROR-${filePath}`,
|
|
279
|
-
severity: "LOW",
|
|
372
|
+
severity: strictCollection ? "HIGH" : "LOW",
|
|
280
373
|
category: "PARSE_ERROR",
|
|
281
374
|
layer: "L1",
|
|
282
375
|
file_path: filePath,
|
|
@@ -455,6 +548,8 @@ export function discoverDeepScanResources(scanTarget, kbInput, options = {}) {
|
|
|
455
548
|
const context = createScanDiscoveryContext(scanTarget, kbInput, {
|
|
456
549
|
includeUserScope: options.includeUserScope,
|
|
457
550
|
homeDir: options.homeDir,
|
|
551
|
+
collectModes: options.collectModes,
|
|
552
|
+
collectKinds: options.collectKinds,
|
|
458
553
|
parseSelected: true,
|
|
459
554
|
});
|
|
460
555
|
return discoverDeepScanResourcesFromContext(context);
|
|
@@ -468,10 +563,17 @@ export function createScanDiscoveryContext(scanTarget, kbInput, options = {}) {
|
|
|
468
563
|
const kb = kbInput ?? loadKnowledgeBase();
|
|
469
564
|
const patterns = gatherCandidatePatterns(kb);
|
|
470
565
|
const walked = walkProjectTree(absoluteTarget);
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
566
|
+
const collectModes = normalizeCollectionModes(options.collectModes);
|
|
567
|
+
const collectKinds = normalizeCollectionKinds(options.collectKinds);
|
|
568
|
+
const explicitOnly = collectModes.size === 1 && collectModes.has("explicit");
|
|
569
|
+
const selected = mergeExplicitCandidates(explicitOnly
|
|
570
|
+
? []
|
|
571
|
+
: collectSelectedCandidates(absoluteTarget, walked.files, patterns, {
|
|
572
|
+
includeUserScope: options.includeUserScope === true,
|
|
573
|
+
homeDir: resolve(options.homeDir ?? homedir()),
|
|
574
|
+
collectModes,
|
|
575
|
+
collectKinds,
|
|
576
|
+
}), options.explicitCandidates, collectKinds);
|
|
475
577
|
return {
|
|
476
578
|
absoluteTarget,
|
|
477
579
|
kb,
|
|
@@ -494,6 +596,8 @@ export function collectScanSurface(scanTarget, kbInput, options = {}) {
|
|
|
494
596
|
const context = createScanDiscoveryContext(scanTarget, kbInput, {
|
|
495
597
|
includeUserScope: options.includeUserScope === true,
|
|
496
598
|
homeDir: options.homeDir,
|
|
599
|
+
collectModes: options.collectModes,
|
|
600
|
+
collectKinds: options.collectKinds,
|
|
497
601
|
});
|
|
498
602
|
const surface = new Set(context.walked.files);
|
|
499
603
|
for (const item of context.selected) {
|
|
@@ -513,6 +617,8 @@ export async function runScanEngine(input) {
|
|
|
513
617
|
const context = input.discoveryContext ??
|
|
514
618
|
createScanDiscoveryContext(input.scanTarget, input.kb, {
|
|
515
619
|
includeUserScope: input.config.scan_user_scope === true,
|
|
620
|
+
collectModes: input.config.scan_collection_modes,
|
|
621
|
+
collectKinds: input.config.scan_collection_kinds,
|
|
516
622
|
homeDir: input.homeDir,
|
|
517
623
|
parseSelected: true,
|
|
518
624
|
});
|
|
@@ -522,7 +628,7 @@ export async function runScanEngine(input) {
|
|
|
522
628
|
const staticFiles = [];
|
|
523
629
|
for (const item of ensureParsedCandidates(context)) {
|
|
524
630
|
if (!item.parsed.ok) {
|
|
525
|
-
parseErrors.push(makeParseErrorFinding(item.reportPath, item.tool, item.parsed.error));
|
|
631
|
+
parseErrors.push(makeParseErrorFinding(item.reportPath, item.tool, item.parsed.error, input.config.strict_collection === true));
|
|
526
632
|
continue;
|
|
527
633
|
}
|
|
528
634
|
staticFiles.push({
|
|
@@ -548,7 +654,7 @@ export async function runScanEngine(input) {
|
|
|
548
654
|
executable: (mode & 0o111) !== 0,
|
|
549
655
|
};
|
|
550
656
|
});
|
|
551
|
-
const report = runStaticPipeline({
|
|
657
|
+
const report = await runStaticPipeline({
|
|
552
658
|
version: input.version,
|
|
553
659
|
kbVersion: kb.schemaVersion,
|
|
554
660
|
scanTarget: input.scanTarget,
|
|
@@ -571,6 +677,13 @@ export async function runScanEngine(input) {
|
|
|
571
677
|
trustedApiDomains: input.config.trusted_api_domains,
|
|
572
678
|
unicodeAnalysis: input.config.unicode_analysis,
|
|
573
679
|
checkIdeSettings: input.config.check_ide_settings,
|
|
680
|
+
rulePackPaths: input.config.rule_pack_paths,
|
|
681
|
+
allowedRules: input.config.allowed_rules,
|
|
682
|
+
skipRules: input.config.skip_rules,
|
|
683
|
+
persona: input.config.persona,
|
|
684
|
+
runtimeMode: input.config.runtime_mode,
|
|
685
|
+
workflowAuditsEnabled: input.config.workflow_audits?.enabled === true,
|
|
686
|
+
rulePolicies: input.config.rules,
|
|
574
687
|
},
|
|
575
688
|
});
|
|
576
689
|
const snapshots = new Map();
|
|
@@ -585,7 +698,11 @@ export async function runScanEngine(input) {
|
|
|
585
698
|
previousState,
|
|
586
699
|
});
|
|
587
700
|
saveScanState(stateResult.nextState, input.scanStatePath);
|
|
588
|
-
const
|
|
701
|
+
const inlineIgnores = collectInlineIgnoreDirectives(staticFiles.map((file) => ({
|
|
702
|
+
filePath: file.filePath,
|
|
703
|
+
textContent: file.textContent,
|
|
704
|
+
})));
|
|
705
|
+
const findings = applyInlineIgnoreDirectives([...report.findings, ...parseErrors, ...stateResult.findings], inlineIgnores);
|
|
589
706
|
return applyReportSummary({
|
|
590
707
|
...report,
|
|
591
708
|
findings,
|
package/dist/types/finding.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare const SEVERITIES: readonly ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"];
|
|
2
2
|
export type Severity = (typeof SEVERITIES)[number];
|
|
3
|
-
export declare const FINDING_CATEGORIES: readonly ["ENV_OVERRIDE", "COMMAND_EXEC", "CONSENT_BYPASS", "RULE_INJECTION", "IDE_SETTINGS", "SYMLINK_ESCAPE", "GIT_HOOK", "CONFIG_PRESENT", "PARSE_ERROR", "CONFIG_CHANGE", "NEW_SERVER", "TOXIC_FLOW"];
|
|
3
|
+
export declare const FINDING_CATEGORIES: readonly ["ENV_OVERRIDE", "COMMAND_EXEC", "CONSENT_BYPASS", "RULE_INJECTION", "IDE_SETTINGS", "SYMLINK_ESCAPE", "GIT_HOOK", "CONFIG_PRESENT", "PARSE_ERROR", "CONFIG_CHANGE", "NEW_SERVER", "TOXIC_FLOW", "CI_SUPPLY_CHAIN", "CI_TRIGGER", "CI_PERMISSIONS", "CI_TEMPLATE_INJECTION", "CI_VULNERABLE_ACTION"];
|
|
4
4
|
export type FindingCategory = (typeof FINDING_CATEGORIES)[number];
|
|
5
5
|
export type FindingLayer = "L1" | "L2" | "L3";
|
|
6
6
|
export type FindingConfidence = "HIGH" | "MEDIUM" | "LOW";
|
|
@@ -13,6 +13,13 @@ export interface FindingSourceConfig {
|
|
|
13
13
|
file_path: string;
|
|
14
14
|
field?: string;
|
|
15
15
|
}
|
|
16
|
+
export interface FindingMetadata {
|
|
17
|
+
sources?: string[];
|
|
18
|
+
sinks?: string[];
|
|
19
|
+
referenced_secrets?: string[];
|
|
20
|
+
risk_tags?: string[];
|
|
21
|
+
origin?: string;
|
|
22
|
+
}
|
|
16
23
|
export interface AffectedLocation {
|
|
17
24
|
file_path: string;
|
|
18
25
|
location?: FindingLocation;
|
|
@@ -20,6 +27,7 @@ export interface AffectedLocation {
|
|
|
20
27
|
export interface Finding {
|
|
21
28
|
rule_id: string;
|
|
22
29
|
finding_id: string;
|
|
30
|
+
fingerprint?: string;
|
|
23
31
|
severity: Severity;
|
|
24
32
|
category: FindingCategory;
|
|
25
33
|
layer: FindingLayer;
|
|
@@ -34,6 +42,7 @@ export interface Finding {
|
|
|
34
42
|
confidence: FindingConfidence;
|
|
35
43
|
fixable: boolean;
|
|
36
44
|
remediation_actions: string[];
|
|
45
|
+
metadata?: FindingMetadata | null;
|
|
37
46
|
evidence?: string | null;
|
|
38
47
|
observed?: string[] | null;
|
|
39
48
|
inference?: string | null;
|
package/dist/types/finding.js
CHANGED