codegate-ai 0.6.1 → 0.8.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.
Files changed (45) hide show
  1. package/README.md +61 -25
  2. package/dist/cli.d.ts +1 -1
  3. package/dist/cli.js +59 -41
  4. package/dist/commands/scan-command/helpers.d.ts +6 -1
  5. package/dist/commands/scan-command/helpers.js +46 -1
  6. package/dist/commands/scan-command.js +49 -55
  7. package/dist/commands/scan-content-command.d.ts +16 -0
  8. package/dist/commands/scan-content-command.js +61 -0
  9. package/dist/config/suppression-policy.d.ts +14 -0
  10. package/dist/config/suppression-policy.js +81 -0
  11. package/dist/config.d.ts +5 -0
  12. package/dist/config.js +29 -3
  13. package/dist/layer2-static/advisories/agent-components.json +62 -0
  14. package/dist/layer2-static/detectors/advisory-intelligence.d.ts +7 -0
  15. package/dist/layer2-static/detectors/advisory-intelligence.js +170 -0
  16. package/dist/layer2-static/detectors/command-exec.js +6 -0
  17. package/dist/layer2-static/detectors/rule-file.js +5 -0
  18. package/dist/layer2-static/engine.d.ts +4 -1
  19. package/dist/layer2-static/engine.js +97 -0
  20. package/dist/layer2-static/rule-engine.d.ts +1 -1
  21. package/dist/layer2-static/rule-engine.js +1 -13
  22. package/dist/layer2-static/rule-pack-loader.d.ts +10 -0
  23. package/dist/layer2-static/rule-pack-loader.js +187 -0
  24. package/dist/layer3-dynamic/command-builder.d.ts +1 -0
  25. package/dist/layer3-dynamic/command-builder.js +44 -2
  26. package/dist/layer3-dynamic/local-text-analysis.d.ts +9 -1
  27. package/dist/layer3-dynamic/local-text-analysis.js +12 -27
  28. package/dist/layer3-dynamic/meta-agent.d.ts +1 -2
  29. package/dist/layer3-dynamic/meta-agent.js +3 -6
  30. package/dist/layer3-dynamic/prompt-templates/local-text-analysis.md +33 -21
  31. package/dist/layer3-dynamic/prompt-templates/security-analysis.md +11 -1
  32. package/dist/layer3-dynamic/prompt-templates/tool-poisoning.md +9 -1
  33. package/dist/layer3-dynamic/toxic-flow.js +6 -0
  34. package/dist/pipeline.js +9 -8
  35. package/dist/report/finding-fingerprint.d.ts +5 -0
  36. package/dist/report/finding-fingerprint.js +47 -0
  37. package/dist/reporter/markdown.js +25 -3
  38. package/dist/reporter/sarif.js +2 -0
  39. package/dist/reporter/terminal.js +25 -0
  40. package/dist/scan-target/fetch-plan.d.ts +8 -0
  41. package/dist/scan-target/fetch-plan.js +30 -0
  42. package/dist/scan-target/staging.js +60 -5
  43. package/dist/scan.js +3 -0
  44. package/dist/types/finding.d.ts +9 -0
  45. package/package.json +3 -1
@@ -49,6 +49,7 @@ function findingToResult(finding) {
49
49
  locations: [findingToLocation(finding)],
50
50
  properties: {
51
51
  finding_id: finding.finding_id,
52
+ fingerprint: finding.fingerprint ?? null,
52
53
  severity: finding.severity,
53
54
  category: finding.category,
54
55
  layer: finding.layer,
@@ -59,6 +60,7 @@ function findingToResult(finding) {
59
60
  evidence: finding.evidence ?? null,
60
61
  fixable: finding.fixable,
61
62
  suppressed: finding.suppressed,
63
+ metadata: finding.metadata ?? null,
62
64
  source_config: finding.source_config ?? null,
63
65
  },
64
66
  };
@@ -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,6 +76,9 @@ 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) {
@@ -70,6 +94,7 @@ function appendFinding(lines, report, options, finding) {
70
94
  if (finding.remediation_actions.length > 0) {
71
95
  lines.push(` Remediation: ${finding.remediation_actions.join(", ")}`);
72
96
  }
97
+ appendMetadata(lines, finding.metadata);
73
98
  }
74
99
  if (finding.layer === "L3" && finding.source_config) {
75
100
  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 cloneRepository(source, destination) {
7
- const result = spawnSync("git", ["clone", "--depth", "1", "--filter=blob:none", source, destination], {
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
- cleanupTempDir(join(destination, ".git"));
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
- cloneRepository(rawTarget, repoDir);
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
- cloneRepository(repoUrl, repoDir);
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.js CHANGED
@@ -571,6 +571,9 @@ export async function runScanEngine(input) {
571
571
  trustedApiDomains: input.config.trusted_api_domains,
572
572
  unicodeAnalysis: input.config.unicode_analysis,
573
573
  checkIdeSettings: input.config.check_ide_settings,
574
+ rulePackPaths: input.config.rule_pack_paths,
575
+ allowedRules: input.config.allowed_rules,
576
+ skipRules: input.config.skip_rules,
574
577
  },
575
578
  });
576
579
  const snapshots = new Map();
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codegate-ai",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "Pre-flight security scanner for AI coding tool configurations.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,6 +31,7 @@
31
31
  "lint": "eslint .",
32
32
  "lint:fix": "eslint . --fix",
33
33
  "test": "vitest run",
34
+ "test:coverage": "vitest run --coverage",
34
35
  "test:perf": "vitest run tests/perf/scan-performance.test.ts",
35
36
  "test:reliability": "vitest run tests/reliability/signal-handling.test.ts",
36
37
  "typecheck": "tsc --noEmit -p tsconfig.json",
@@ -85,6 +86,7 @@
85
86
  "@types/node": "^25.3.5",
86
87
  "@types/react": "^19.2.14",
87
88
  "@types/which": "^3.0.4",
89
+ "@vitest/coverage-v8": "^4.1.0",
88
90
  "conventional-changelog-conventionalcommits": "^9.3.0",
89
91
  "eslint": "^10.0.2",
90
92
  "globals": "^17.4.0",