codegate-ai 0.16.2 → 1.0.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/dist/cli.d.ts +3 -1
- package/dist/cli.js +153 -44
- package/dist/commands/scan-command.d.ts +2 -1
- package/dist/commands/scan-command.js +6 -1
- package/dist/commands/trust.d.ts +28 -0
- package/dist/commands/trust.js +69 -0
- package/dist/config/inline-ignore.d.ts +10 -2
- package/dist/config/inline-ignore.js +5 -1
- package/dist/config/suppression-policy.d.ts +1 -1
- package/dist/config/suppression-policy.js +4 -1
- package/dist/config/trust.d.ts +2 -0
- package/dist/config/trust.js +19 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +45 -1
- package/dist/content/content-bundle.d.ts +27 -0
- package/dist/content/content-bundle.js +73 -0
- package/dist/content/content-store.d.ts +27 -0
- package/dist/content/content-store.js +0 -0
- package/dist/content/content-updater.d.ts +32 -0
- package/dist/content/content-updater.js +111 -0
- package/dist/content/known-bad.d.ts +26 -0
- package/dist/content/known-bad.js +100 -0
- package/dist/content/publisher-key.d.ts +10 -0
- package/dist/content/publisher-key.js +10 -0
- package/dist/layer1-discovery/knowledge-base.js +33 -1
- package/dist/layer2-static/data/popular-mcp-packages.d.ts +16 -0
- package/dist/layer2-static/data/popular-mcp-packages.js +83 -0
- package/dist/layer2-static/detectors/known-bad.d.ts +22 -0
- package/dist/layer2-static/detectors/known-bad.js +116 -0
- package/dist/layer2-static/detectors/mcp-package-hygiene.d.ts +28 -0
- package/dist/layer2-static/detectors/mcp-package-hygiene.js +191 -0
- package/dist/layer2-static/detectors/rule-file.js +128 -75
- package/dist/layer2-static/detectors/skill-frontmatter.d.ts +6 -0
- package/dist/layer2-static/detectors/skill-frontmatter.js +130 -0
- package/dist/layer2-static/engine.d.ts +2 -0
- package/dist/layer2-static/engine.js +0 -0
- package/dist/layer2-static/rule-pack-loader.js +24 -1
- package/dist/layer2-static/state/scan-state.d.ts +7 -2
- package/dist/layer2-static/state/scan-state.js +74 -30
- package/dist/layer2-static/text/confusables.d.ts +7 -0
- package/dist/layer2-static/text/confusables.js +70 -0
- package/dist/layer2-static/text/edit-distance.d.ts +6 -0
- package/dist/layer2-static/text/edit-distance.js +33 -0
- package/dist/layer2-static/text/encoded-payloads.d.ts +16 -0
- package/dist/layer2-static/text/encoded-payloads.js +116 -0
- package/dist/layer2-static/text/normalize.d.ts +11 -0
- package/dist/layer2-static/text/normalize.js +19 -0
- package/dist/layer2-static/text/override-phrases.d.ts +14 -0
- package/dist/layer2-static/text/override-phrases.js +49 -0
- package/dist/layer2-static/text/threat-patterns.d.ts +33 -0
- package/dist/layer2-static/text/threat-patterns.js +45 -0
- package/dist/layer2-static/text/unicode.d.ts +33 -0
- package/dist/layer2-static/text/unicode.js +83 -0
- package/dist/layer3-dynamic/deep-resource-executor.d.ts +21 -0
- package/dist/layer3-dynamic/deep-resource-executor.js +73 -0
- package/dist/layer3-dynamic/meta-agent.js +2 -1
- package/dist/layer3-dynamic/registry-client.d.ts +26 -0
- package/dist/layer3-dynamic/registry-client.js +138 -0
- package/dist/layer3-dynamic/registry-findings.d.ts +7 -0
- package/dist/layer3-dynamic/registry-findings.js +64 -0
- package/dist/layer3-dynamic/tool-description-scanner.js +22 -13
- package/dist/layer3-dynamic/toxic-flow.d.ts +4 -0
- package/dist/layer3-dynamic/toxic-flow.js +41 -8
- package/dist/pipeline.d.ts +5 -2
- package/dist/pipeline.js +53 -16
- package/dist/report-summary.d.ts +1 -1
- package/dist/report-summary.js +9 -1
- package/dist/scan.d.ts +13 -0
- package/dist/scan.js +291 -17
- package/dist/types/finding.d.ts +14 -0
- package/dist/types/finding.js +14 -0
- package/dist/types/report.d.ts +2 -0
- package/dist/wrapper.js +2 -19
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { type CodeGateConfig, type ResolveConfigOptions } from "./config.js";
|
|
4
|
+
import { type DeepResourceExecutionContext } from "./layer3-dynamic/deep-resource-executor.js";
|
|
4
5
|
import type { ResourceFetchResult } from "./layer3-dynamic/resource-fetcher.js";
|
|
5
6
|
import type { LocalTextAnalysisTarget } from "./layer3-dynamic/local-text-analysis.js";
|
|
6
7
|
import { type DeepScanResource } from "./pipeline.js";
|
|
@@ -55,8 +56,9 @@ export interface CliDeps {
|
|
|
55
56
|
runMetaAgentCommand?: (context: MetaAgentCommandConsentContext) => Promise<MetaAgentCommandRunResult> | MetaAgentCommandRunResult;
|
|
56
57
|
requestRemediationConsent?: (context: RemediationConsentContext) => Promise<boolean> | boolean;
|
|
57
58
|
requestRunWarningConsent?: (context: RunWarningConsentContext) => Promise<boolean> | boolean;
|
|
59
|
+
requestTrustConsent?: (directory: string) => Promise<boolean> | boolean;
|
|
58
60
|
requestSkillSelection?: (options: string[]) => Promise<string | null> | string | null;
|
|
59
|
-
executeDeepResource?: (resource: DeepScanResource) => Promise<ResourceFetchResult>;
|
|
61
|
+
executeDeepResource?: (resource: DeepScanResource, context?: DeepResourceExecutionContext) => Promise<ResourceFetchResult>;
|
|
60
62
|
launchSkills?: (args: string[], cwd: string) => SkillsWrapperLaunchResult;
|
|
61
63
|
launchClawhub?: (args: string[], cwd: string) => ClawhubWrapperLaunchResult;
|
|
62
64
|
runSkillsWrapper?: (input: {
|
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { pathToFileURL } from "node:url";
|
|
|
8
8
|
import { Command, Option } from "commander";
|
|
9
9
|
import { DEFAULT_CONFIG, OUTPUT_FORMATS, PERSONAS, RUNTIME_MODES, SCAN_COLLECTION_MODES, SCAN_COLLECTION_KINDS, resolveEffectiveConfig, } from "./config.js";
|
|
10
10
|
import { APP_NAME } from "./index.js";
|
|
11
|
+
import { createDeepResourceExecutor, } from "./layer3-dynamic/deep-resource-executor.js";
|
|
11
12
|
import { runSandboxCommand } from "./layer3-dynamic/sandbox.js";
|
|
12
13
|
import { runClaudeViaSdk } from "./layer3-dynamic/claude-sdk-provider.js";
|
|
13
14
|
import { loadKnowledgeBase } from "./layer1-discovery/knowledge-base.js";
|
|
@@ -18,6 +19,8 @@ import { renderTuiApp } from "./tui/app.js";
|
|
|
18
19
|
import { executeWrapperRun } from "./wrapper.js";
|
|
19
20
|
import { runRemediation as runRemediationWorkflow, } from "./layer4-remediation/remediation-runner.js";
|
|
20
21
|
import { undoLatestSession } from "./commands/undo.js";
|
|
22
|
+
import { addTrustedDirectory, listTrustedDirectories, removeTrustedDirectory, } from "./commands/trust.js";
|
|
23
|
+
import { checkContentUpdate, rollbackContent, updateContent } from "./content/content-updater.js";
|
|
21
24
|
import { runInventory } from "./commands/inventory-command.js";
|
|
22
25
|
import { executeScanCommand } from "./commands/scan-command.js";
|
|
23
26
|
import { executeScanContentCommand, SCAN_CONTENT_TYPES, } from "./commands/scan-content-command.js";
|
|
@@ -206,23 +209,10 @@ const defaultCliDeps = {
|
|
|
206
209
|
collectKinds: config?.scan_collection_kinds,
|
|
207
210
|
}),
|
|
208
211
|
discoverLocalTextTargets: (_scanTarget, _config, discoveryContext) => discoveryContext ? discoverLocalTextAnalysisTargetsFromContext(discoveryContext) : [],
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
executeDeepResource: async (resource) => {
|
|
214
|
-
return {
|
|
215
|
-
status: "ok",
|
|
216
|
-
attempts: 0,
|
|
217
|
-
elapsedMs: 0,
|
|
218
|
-
metadata: {
|
|
219
|
-
resource_id: resource.id,
|
|
220
|
-
resource_kind: resource.request.kind,
|
|
221
|
-
resource_url: resource.request.locator,
|
|
222
|
-
note: "URL recorded for analysis without making outbound connections.",
|
|
223
|
-
},
|
|
224
|
-
};
|
|
225
|
-
},
|
|
212
|
+
// URL resources are never fetched (SSRF/IP-logging risk); npm/pypi registry
|
|
213
|
+
// metadata is fetched from pinned hosts only when runtime_mode is "online".
|
|
214
|
+
// See createDeepResourceExecutor for the full policy.
|
|
215
|
+
executeDeepResource: createDeepResourceExecutor(),
|
|
226
216
|
launchSkills: (args, cwd) => launchSkillsPassthrough(args, cwd),
|
|
227
217
|
launchClawhub: (args, cwd) => launchClawhubPassthrough(args, cwd),
|
|
228
218
|
};
|
|
@@ -604,6 +594,100 @@ function addClawhubCommand(program, version, deps) {
|
|
|
604
594
|
}
|
|
605
595
|
});
|
|
606
596
|
}
|
|
597
|
+
async function promptTrustConsent(directory) {
|
|
598
|
+
const rl = createInterface({
|
|
599
|
+
input: process.stdin,
|
|
600
|
+
output: process.stdout,
|
|
601
|
+
});
|
|
602
|
+
const prompt = [
|
|
603
|
+
`Trusting ${directory} lets its .codegate.json set scanner policy`,
|
|
604
|
+
"(allowlists, rule skips, suppressions) and relaxes launch gating for it.",
|
|
605
|
+
"Only trust directories whose contents you control.",
|
|
606
|
+
"Proceed? [y/N]: ",
|
|
607
|
+
].join("\n");
|
|
608
|
+
try {
|
|
609
|
+
const answer = await rl.question(prompt);
|
|
610
|
+
return /^y(es)?$/iu.test(answer.trim());
|
|
611
|
+
}
|
|
612
|
+
finally {
|
|
613
|
+
rl.close();
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function addTrustCommand(program, deps) {
|
|
617
|
+
program
|
|
618
|
+
.command("trust [dir]")
|
|
619
|
+
.description("Manage trusted directories where project config policy is honored")
|
|
620
|
+
.option("--list", "list trusted directories")
|
|
621
|
+
.option("--remove <dir>", "remove a directory from the trusted list")
|
|
622
|
+
.option("--config <path>", "use a specific global config file")
|
|
623
|
+
.option("--yes", "skip the confirmation prompt")
|
|
624
|
+
.addHelpText("after", renderExampleHelp([
|
|
625
|
+
"codegate trust",
|
|
626
|
+
"codegate trust ./project --yes",
|
|
627
|
+
"codegate trust --list",
|
|
628
|
+
"codegate trust --remove ./project",
|
|
629
|
+
]))
|
|
630
|
+
.action(async (dir, options) => {
|
|
631
|
+
try {
|
|
632
|
+
if (options.list) {
|
|
633
|
+
const listed = listTrustedDirectories({ configPath: options.config });
|
|
634
|
+
if (listed.trustedDirectories.length === 0) {
|
|
635
|
+
deps.stdout(`No trusted directories configured (${listed.configPath}).`);
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
deps.stdout(`Trusted directories (${listed.configPath}):`);
|
|
639
|
+
for (const entry of listed.trustedDirectories) {
|
|
640
|
+
deps.stdout(` ${entry}`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
deps.setExitCode(0);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (options.remove) {
|
|
647
|
+
const removed = removeTrustedDirectory({
|
|
648
|
+
dir: options.remove,
|
|
649
|
+
cwd: deps.cwd(),
|
|
650
|
+
configPath: options.config,
|
|
651
|
+
});
|
|
652
|
+
deps.stdout(removed.changed
|
|
653
|
+
? `Removed ${removed.directory} from trusted directories.`
|
|
654
|
+
: `${removed.directory} was not in the trusted list.`);
|
|
655
|
+
deps.setExitCode(0);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const targetDir = dir ?? ".";
|
|
659
|
+
const resolvedPreview = resolve(deps.cwd(), targetDir);
|
|
660
|
+
if (!options.yes) {
|
|
661
|
+
const requestConsent = deps.requestTrustConsent ?? (deps.isTTY() ? promptTrustConsent : undefined);
|
|
662
|
+
if (!requestConsent) {
|
|
663
|
+
deps.stderr("Confirmation required. Re-run with --yes to trust non-interactively.");
|
|
664
|
+
deps.setExitCode(3);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
const approved = await requestConsent(resolvedPreview);
|
|
668
|
+
if (!approved) {
|
|
669
|
+
deps.stdout("Trust not granted.");
|
|
670
|
+
deps.setExitCode(1);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const added = addTrustedDirectory({
|
|
675
|
+
dir: targetDir,
|
|
676
|
+
cwd: deps.cwd(),
|
|
677
|
+
configPath: options.config,
|
|
678
|
+
});
|
|
679
|
+
deps.stdout(added.changed
|
|
680
|
+
? `Trusted ${added.directory} (${added.configPath}).`
|
|
681
|
+
: `${added.directory} is already trusted.`);
|
|
682
|
+
deps.setExitCode(0);
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
686
|
+
deps.stderr(`Trust failed: ${message}`);
|
|
687
|
+
deps.setExitCode(3);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
}
|
|
607
691
|
function addUndoCommand(program, deps) {
|
|
608
692
|
program
|
|
609
693
|
.command("undo [dir]")
|
|
@@ -723,33 +807,57 @@ function renderInventoryText(summary, stdout) {
|
|
|
723
807
|
}
|
|
724
808
|
}
|
|
725
809
|
function addUpdateCommands(program, deps) {
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
810
|
+
const registerUpdateCommand = (name, description) => {
|
|
811
|
+
program
|
|
812
|
+
.command(name)
|
|
813
|
+
.description(description)
|
|
814
|
+
.option("--check", "check for a newer signed content bundle without installing")
|
|
815
|
+
.option("--rollback", "switch back to the previously installed content version")
|
|
816
|
+
.option("--url <baseUrl>", "override the content download base URL (the signature is still required to verify)")
|
|
817
|
+
.addHelpText("after", renderExampleHelp([
|
|
818
|
+
`codegate ${name}`,
|
|
819
|
+
`codegate ${name} --check`,
|
|
820
|
+
`codegate ${name} --rollback`,
|
|
821
|
+
]))
|
|
822
|
+
.action(async (options) => {
|
|
823
|
+
try {
|
|
824
|
+
if (options.rollback) {
|
|
825
|
+
const rolledBack = rollbackContent();
|
|
826
|
+
deps.stdout(`Rolled back to content version ${rolledBack.version}.`);
|
|
827
|
+
deps.setExitCode(0);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
if (options.check) {
|
|
831
|
+
const checked = await checkContentUpdate({}, { baseUrl: options.url });
|
|
832
|
+
deps.stdout(`Installed content: ${checked.currentVersion ?? "bundled only"}`);
|
|
833
|
+
deps.stdout(`Latest published: ${checked.remoteVersion}`);
|
|
834
|
+
deps.stdout(checked.updateAvailable
|
|
835
|
+
? `Update available. Run: codegate ${name}`
|
|
836
|
+
: "Content is up to date.");
|
|
837
|
+
deps.setExitCode(0);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const updated = await updateContent({}, { baseUrl: options.url });
|
|
841
|
+
if (updated.changed) {
|
|
842
|
+
deps.stdout(`Updated content: ${updated.previousVersion ?? "bundled only"} -> ${updated.version}`);
|
|
843
|
+
if (updated.pruned.length > 0) {
|
|
844
|
+
deps.stdout(`Pruned old versions: ${updated.pruned.join(", ")}`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
deps.stdout(`Content already up to date (${updated.version}).`);
|
|
849
|
+
}
|
|
850
|
+
deps.setExitCode(0);
|
|
851
|
+
}
|
|
852
|
+
catch (error) {
|
|
853
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
854
|
+
deps.stderr(`${name} failed: ${message}`);
|
|
855
|
+
deps.setExitCode(3);
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
};
|
|
859
|
+
registerUpdateCommand("update-kb", "Fetch and verify the signed content bundle (knowledge base, rules, phrase lists)");
|
|
860
|
+
registerUpdateCommand("update-rules", "Alias of update-kb: content ships as one signed bundle");
|
|
753
861
|
}
|
|
754
862
|
function resolveKnowledgeBaseVersion() {
|
|
755
863
|
try {
|
|
@@ -781,6 +889,7 @@ export function createCli(version = packageJson.version ?? "0.0.0-dev", deps = d
|
|
|
781
889
|
addSkillsCommand(program, version, deps);
|
|
782
890
|
addClawhubCommand(program, version, deps);
|
|
783
891
|
addRunCommand(program, version, deps);
|
|
892
|
+
addTrustCommand(program, deps);
|
|
784
893
|
addUndoCommand(program, deps);
|
|
785
894
|
addInitCommand(program, deps);
|
|
786
895
|
addInventoryCommand(program, deps);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type AuditPersona, type CodeGateConfig, type OutputFormat, type RuntimeMode, type ScanCollectionMode } from "../config.js";
|
|
2
2
|
import { type MetaAgentCommand, type MetaAgentTool } from "../layer3-dynamic/command-builder.js";
|
|
3
3
|
import type { LocalTextAnalysisTarget } from "../layer3-dynamic/local-text-analysis.js";
|
|
4
|
+
import type { DeepResourceExecutionContext } from "../layer3-dynamic/deep-resource-executor.js";
|
|
4
5
|
import type { ResourceFetchResult } from "../layer3-dynamic/resource-fetcher.js";
|
|
5
6
|
import { type DeepScanResource } from "../pipeline.js";
|
|
6
7
|
import type { ScanDiscoveryCandidate, ScanDiscoveryContext } from "../scan.js";
|
|
@@ -81,7 +82,7 @@ export interface ExecuteScanCommandDeps {
|
|
|
81
82
|
requestMetaAgentCommandConsent?: (context: MetaAgentCommandConsentContext) => Promise<boolean> | boolean;
|
|
82
83
|
runMetaAgentCommand?: (context: MetaAgentCommandConsentContext) => Promise<MetaAgentCommandRunResult> | MetaAgentCommandRunResult;
|
|
83
84
|
requestRemediationConsent?: (context: RemediationConsentContext) => Promise<boolean> | boolean;
|
|
84
|
-
executeDeepResource?: (resource: DeepScanResource) => Promise<ResourceFetchResult>;
|
|
85
|
+
executeDeepResource?: (resource: DeepScanResource, context?: DeepResourceExecutionContext) => Promise<ResourceFetchResult>;
|
|
85
86
|
runRemediation?: (input: RemediationRunnerInput) => Promise<RemediationRunnerResult> | RemediationRunnerResult;
|
|
86
87
|
stdout: (message: string) => void;
|
|
87
88
|
stderr: (message: string) => void;
|
|
@@ -130,7 +130,9 @@ export async function runScanAnalysis(input, deps) {
|
|
|
130
130
|
}
|
|
131
131
|
return false;
|
|
132
132
|
}, async (resource) => {
|
|
133
|
-
const fetched = await deps.executeDeepResource(resource
|
|
133
|
+
const fetched = await deps.executeDeepResource(resource, {
|
|
134
|
+
runtimeMode: input.config.runtime_mode,
|
|
135
|
+
});
|
|
134
136
|
if (fetched.status !== "ok" || !selectedAgent) {
|
|
135
137
|
return fetched;
|
|
136
138
|
}
|
|
@@ -202,6 +204,9 @@ export async function runScanAnalysis(input, deps) {
|
|
|
202
204
|
});
|
|
203
205
|
const layer3Findings = layer3OutcomesToFindings(outcomes, {
|
|
204
206
|
unicodeAnalysis: input.config.unicode_analysis,
|
|
207
|
+
registryHeuristics: {
|
|
208
|
+
recentPublishDays: input.config.registry_heuristics?.recent_publish_days,
|
|
209
|
+
},
|
|
205
210
|
});
|
|
206
211
|
report = mergeLayer3Findings(report, layer3Findings);
|
|
207
212
|
if (selectedAgent) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface TrustStoreDeps {
|
|
2
|
+
homeDir: () => string;
|
|
3
|
+
pathExists: (path: string) => boolean;
|
|
4
|
+
readFile: (path: string) => string;
|
|
5
|
+
writeFile: (path: string, content: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export interface TrustActionResult {
|
|
8
|
+
configPath: string;
|
|
9
|
+
directory: string;
|
|
10
|
+
changed: boolean;
|
|
11
|
+
trustedDirectories: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function listTrustedDirectories(input?: {
|
|
14
|
+
configPath?: string;
|
|
15
|
+
}, deps?: TrustStoreDeps): {
|
|
16
|
+
configPath: string;
|
|
17
|
+
trustedDirectories: string[];
|
|
18
|
+
};
|
|
19
|
+
export declare function addTrustedDirectory(input: {
|
|
20
|
+
dir: string;
|
|
21
|
+
cwd: string;
|
|
22
|
+
configPath?: string;
|
|
23
|
+
}, deps?: TrustStoreDeps): TrustActionResult;
|
|
24
|
+
export declare function removeTrustedDirectory(input: {
|
|
25
|
+
dir: string;
|
|
26
|
+
cwd: string;
|
|
27
|
+
configPath?: string;
|
|
28
|
+
}, deps?: TrustStoreDeps): TrustActionResult;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { parse as parseJsonc } from "jsonc-parser";
|
|
5
|
+
import { expandHomePath } from "../config/trust.js";
|
|
6
|
+
const defaultTrustStoreDeps = {
|
|
7
|
+
homeDir: () => homedir(),
|
|
8
|
+
pathExists: (path) => existsSync(path),
|
|
9
|
+
readFile: (path) => readFileSync(path, "utf8"),
|
|
10
|
+
writeFile: (path, content) => {
|
|
11
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
12
|
+
writeFileSync(path, content, "utf8");
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
function resolveConfigPath(configPath, deps) {
|
|
16
|
+
return configPath ?? join(deps.homeDir(), ".codegate", "config.json");
|
|
17
|
+
}
|
|
18
|
+
function readConfigObject(path, deps) {
|
|
19
|
+
if (!deps.pathExists(path)) {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
const parsed = parseJsonc(deps.readFile(path));
|
|
23
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
24
|
+
throw new Error(`Invalid config file: ${path}`);
|
|
25
|
+
}
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
function readTrustedList(config) {
|
|
29
|
+
const raw = config.trusted_directories;
|
|
30
|
+
if (!Array.isArray(raw)) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
return raw.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
34
|
+
}
|
|
35
|
+
function writeConfigObject(path, config, deps) {
|
|
36
|
+
deps.writeFile(path, `${JSON.stringify(config, null, 2)}\n`);
|
|
37
|
+
}
|
|
38
|
+
function sameDirectory(left, right) {
|
|
39
|
+
return resolve(expandHomePath(left)) === resolve(expandHomePath(right));
|
|
40
|
+
}
|
|
41
|
+
export function listTrustedDirectories(input = {}, deps = defaultTrustStoreDeps) {
|
|
42
|
+
const configPath = resolveConfigPath(input.configPath, deps);
|
|
43
|
+
const config = readConfigObject(configPath, deps);
|
|
44
|
+
return { configPath, trustedDirectories: readTrustedList(config) };
|
|
45
|
+
}
|
|
46
|
+
export function addTrustedDirectory(input, deps = defaultTrustStoreDeps) {
|
|
47
|
+
const directory = resolve(input.cwd, expandHomePath(input.dir));
|
|
48
|
+
const configPath = resolveConfigPath(input.configPath, deps);
|
|
49
|
+
const config = readConfigObject(configPath, deps);
|
|
50
|
+
const trusted = readTrustedList(config);
|
|
51
|
+
if (trusted.some((entry) => sameDirectory(entry, directory))) {
|
|
52
|
+
return { configPath, directory, changed: false, trustedDirectories: trusted };
|
|
53
|
+
}
|
|
54
|
+
const next = [...trusted, directory];
|
|
55
|
+
writeConfigObject(configPath, { ...config, trusted_directories: next }, deps);
|
|
56
|
+
return { configPath, directory, changed: true, trustedDirectories: next };
|
|
57
|
+
}
|
|
58
|
+
export function removeTrustedDirectory(input, deps = defaultTrustStoreDeps) {
|
|
59
|
+
const directory = resolve(input.cwd, expandHomePath(input.dir));
|
|
60
|
+
const configPath = resolveConfigPath(input.configPath, deps);
|
|
61
|
+
const config = readConfigObject(configPath, deps);
|
|
62
|
+
const trusted = readTrustedList(config);
|
|
63
|
+
const next = trusted.filter((entry) => !sameDirectory(entry, directory));
|
|
64
|
+
if (next.length === trusted.length) {
|
|
65
|
+
return { configPath, directory, changed: false, trustedDirectories: trusted };
|
|
66
|
+
}
|
|
67
|
+
writeConfigObject(configPath, { ...config, trusted_directories: next }, deps);
|
|
68
|
+
return { configPath, directory, changed: true, trustedDirectories: next };
|
|
69
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Finding } from "../types/finding.js";
|
|
2
2
|
export interface InlineIgnoreDirectiveSet {
|
|
3
3
|
rules: Set<string>;
|
|
4
4
|
ruleLines: Map<string, Set<number>>;
|
|
@@ -8,4 +8,12 @@ export declare function collectInlineIgnoreDirectives(files: Array<{
|
|
|
8
8
|
filePath: string;
|
|
9
9
|
textContent: string;
|
|
10
10
|
}>): InlineIgnoreMap;
|
|
11
|
-
export
|
|
11
|
+
export interface ApplyInlineIgnoreOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Whether the scanned target is trusted. Inline directives in untrusted
|
|
14
|
+
* content are recorded but must not weaken gating, so they are tagged
|
|
15
|
+
* "inline-untrusted" and still count toward the exit code.
|
|
16
|
+
*/
|
|
17
|
+
trustedTarget: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function applyInlineIgnoreDirectives<T extends Finding>(findings: T[], directives: InlineIgnoreMap, options?: ApplyInlineIgnoreOptions): T[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SUPPRESSION_SOURCE } from "../types/finding.js";
|
|
1
2
|
function normalizeRuleId(value) {
|
|
2
3
|
const trimmed = value.trim();
|
|
3
4
|
return trimmed.length > 0 ? trimmed : null;
|
|
@@ -43,7 +44,7 @@ export function collectInlineIgnoreDirectives(files) {
|
|
|
43
44
|
}
|
|
44
45
|
return directives;
|
|
45
46
|
}
|
|
46
|
-
export function applyInlineIgnoreDirectives(findings, directives) {
|
|
47
|
+
export function applyInlineIgnoreDirectives(findings, directives, options = { trustedTarget: true }) {
|
|
47
48
|
return findings.map((finding) => {
|
|
48
49
|
const set = directives.get(finding.file_path);
|
|
49
50
|
if (!set || !set.rules.has(finding.rule_id)) {
|
|
@@ -52,6 +53,9 @@ export function applyInlineIgnoreDirectives(findings, directives) {
|
|
|
52
53
|
return {
|
|
53
54
|
...finding,
|
|
54
55
|
suppressed: true,
|
|
56
|
+
suppression_source: options.trustedTarget
|
|
57
|
+
? SUPPRESSION_SOURCE.Inline
|
|
58
|
+
: SUPPRESSION_SOURCE.InlineUntrusted,
|
|
55
59
|
};
|
|
56
60
|
});
|
|
57
61
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SUPPRESSION_SOURCE } from "../types/finding.js";
|
|
1
2
|
function normalizeString(value) {
|
|
2
3
|
if (typeof value !== "string") {
|
|
3
4
|
return undefined;
|
|
@@ -138,9 +139,11 @@ export function applySuppressionPolicy(findings, policy) {
|
|
|
138
139
|
const rulePolicy = rulePolicies[finding.rule_id];
|
|
139
140
|
const ruleDisabled = rulePolicy?.disable === true;
|
|
140
141
|
const ruleIgnoreMatch = rulePolicy?.ignore?.some((location) => matchesRulePolicyIgnore(finding, location)) ?? false;
|
|
142
|
+
const policyMatch = legacyMatch || ruleMatch || ruleDisabled || ruleIgnoreMatch;
|
|
141
143
|
return {
|
|
142
144
|
...finding,
|
|
143
|
-
suppressed: finding.suppressed ||
|
|
145
|
+
suppressed: finding.suppressed || policyMatch,
|
|
146
|
+
suppression_source: finding.suppression_source ?? (policyMatch ? SUPPRESSION_SOURCE.Config : undefined),
|
|
144
147
|
};
|
|
145
148
|
});
|
|
146
149
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
export function expandHomePath(path) {
|
|
4
|
+
if (path === "~") {
|
|
5
|
+
return homedir();
|
|
6
|
+
}
|
|
7
|
+
if (path.startsWith(`~${sep}`) || path.startsWith("~/")) {
|
|
8
|
+
return resolve(homedir(), path.slice(2));
|
|
9
|
+
}
|
|
10
|
+
return path;
|
|
11
|
+
}
|
|
12
|
+
export function isTrustedDirectory(target, trustedDirectories) {
|
|
13
|
+
const resolvedTarget = resolve(target);
|
|
14
|
+
return trustedDirectories.some((trustedPath) => {
|
|
15
|
+
const resolvedTrusted = resolve(expandHomePath(trustedPath));
|
|
16
|
+
const rel = relative(resolvedTrusted, resolvedTarget);
|
|
17
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
18
|
+
});
|
|
19
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -26,6 +26,9 @@ export interface ToolDiscoveryConfig {
|
|
|
26
26
|
export interface WorkflowAuditConfig {
|
|
27
27
|
enabled: boolean;
|
|
28
28
|
}
|
|
29
|
+
export interface RegistryHeuristicsConfig {
|
|
30
|
+
recent_publish_days: number;
|
|
31
|
+
}
|
|
29
32
|
export interface CodeGateConfig {
|
|
30
33
|
severity_threshold: SeverityThreshold;
|
|
31
34
|
auto_proceed_below_threshold: boolean;
|
|
@@ -42,6 +45,8 @@ export interface CodeGateConfig {
|
|
|
42
45
|
known_safe_hooks: string[];
|
|
43
46
|
unicode_analysis: boolean;
|
|
44
47
|
check_ide_settings: boolean;
|
|
48
|
+
/** Emit review findings for MCP servers seen for the first time in a project. */
|
|
49
|
+
first_scan_review?: boolean;
|
|
45
50
|
owasp_mapping: boolean;
|
|
46
51
|
trusted_api_domains: string[];
|
|
47
52
|
rule_pack_paths?: string[];
|
|
@@ -54,6 +59,7 @@ export interface CodeGateConfig {
|
|
|
54
59
|
persona?: AuditPersona;
|
|
55
60
|
runtime_mode?: RuntimeMode;
|
|
56
61
|
workflow_audits?: WorkflowAuditConfig;
|
|
62
|
+
registry_heuristics?: RegistryHeuristicsConfig;
|
|
57
63
|
suppress_findings: string[];
|
|
58
64
|
suppression_rules?: SuppressionRule[];
|
|
59
65
|
/**
|
|
@@ -72,6 +78,10 @@ export interface CodeGateConfig {
|
|
|
72
78
|
* `CODEGATE_LAYER3_REMOTE_FETCH_MAX_BYTES`.
|
|
73
79
|
*/
|
|
74
80
|
layer3_remote_fetch_max_bytes: number;
|
|
81
|
+
/** True when the scan target sits inside a globally trusted directory. */
|
|
82
|
+
project_config_trusted?: boolean;
|
|
83
|
+
/** Policy keys present in the target's .codegate.json that were ignored because the target is untrusted. */
|
|
84
|
+
ignored_project_settings?: string[];
|
|
75
85
|
}
|
|
76
86
|
export interface CliConfigOverrides {
|
|
77
87
|
format?: OutputFormat;
|
|
@@ -88,6 +98,10 @@ export declare const DEFAULT_CONFIG: CodeGateConfig;
|
|
|
88
98
|
export declare const LAYER3_REMOTE_FETCH_TIMEOUT_ENV = "CODEGATE_LAYER3_REMOTE_FETCH_TIMEOUT_MS";
|
|
89
99
|
/** Env var name that overrides `layer3_remote_fetch_max_bytes`. */
|
|
90
100
|
export declare const LAYER3_REMOTE_FETCH_MAX_BYTES_ENV = "CODEGATE_LAYER3_REMOTE_FETCH_MAX_BYTES";
|
|
101
|
+
export declare const PROJECT_COSMETIC_KEYS: readonly ["output_format", "tui", "owasp_mapping"];
|
|
102
|
+
export type ProjectCosmeticKey = (typeof PROJECT_COSMETIC_KEYS)[number];
|
|
103
|
+
export declare const PROJECT_FORBIDDEN_KEYS: readonly ["trusted_directories"];
|
|
104
|
+
export type ProjectForbiddenKey = (typeof PROJECT_FORBIDDEN_KEYS)[number];
|
|
91
105
|
export declare function resolveEffectiveConfig(options: ResolveConfigOptions): CodeGateConfig;
|
|
92
106
|
export declare function computeExitCode(findings: Finding[], threshold: SeverityThreshold): number;
|
|
93
107
|
export declare function applyConfigPolicy(report: CodeGateReport, config: CodeGateConfig): CodeGateReport;
|
package/dist/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { parse as parseJsonc } from "jsonc-parser";
|
|
5
|
+
import { isTrustedDirectory } from "./config/trust.js";
|
|
5
6
|
import { applyReportSummary, computeExitCode as computeReportExitCode } from "./report-summary.js";
|
|
6
7
|
import { applySuppressionPolicy, } from "./config/suppression-policy.js";
|
|
7
8
|
export const OUTPUT_FORMATS = ["terminal", "json", "sarif", "markdown", "html"];
|
|
@@ -37,6 +38,7 @@ export const DEFAULT_CONFIG = {
|
|
|
37
38
|
known_safe_hooks: [],
|
|
38
39
|
unicode_analysis: true,
|
|
39
40
|
check_ide_settings: true,
|
|
41
|
+
first_scan_review: true,
|
|
40
42
|
owasp_mapping: true,
|
|
41
43
|
trusted_api_domains: [],
|
|
42
44
|
rule_pack_paths: [],
|
|
@@ -47,6 +49,7 @@ export const DEFAULT_CONFIG = {
|
|
|
47
49
|
persona: "regular",
|
|
48
50
|
runtime_mode: "offline",
|
|
49
51
|
workflow_audits: { enabled: false },
|
|
52
|
+
registry_heuristics: { recent_publish_days: 30 },
|
|
50
53
|
suppress_findings: [],
|
|
51
54
|
suppression_rules: [],
|
|
52
55
|
layer3_remote_fetch_timeout_ms: 5000,
|
|
@@ -270,12 +273,47 @@ function pickFirst(...values) {
|
|
|
270
273
|
}
|
|
271
274
|
return undefined;
|
|
272
275
|
}
|
|
276
|
+
// Presentation-only keys the scan target's .codegate.json may always set.
|
|
277
|
+
// Every other key is policy: it changes what is detected, suppressed,
|
|
278
|
+
// executed, or gated, so untrusted targets must not control it.
|
|
279
|
+
export const PROJECT_COSMETIC_KEYS = ["output_format", "tui", "owasp_mapping"];
|
|
280
|
+
// Keys never honored from project config, whether the target is trusted or not.
|
|
281
|
+
export const PROJECT_FORBIDDEN_KEYS = ["trusted_directories"];
|
|
282
|
+
function isProjectCosmeticKey(key) {
|
|
283
|
+
return PROJECT_COSMETIC_KEYS.includes(key);
|
|
284
|
+
}
|
|
285
|
+
function isProjectForbiddenKey(key) {
|
|
286
|
+
return PROJECT_FORBIDDEN_KEYS.includes(key);
|
|
287
|
+
}
|
|
288
|
+
function isProjectKeyHonored(key, trusted) {
|
|
289
|
+
if (isProjectForbiddenKey(key)) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
return trusted || isProjectCosmeticKey(key);
|
|
293
|
+
}
|
|
294
|
+
function partitionProjectConfig(projectConfig, trusted) {
|
|
295
|
+
const effective = {};
|
|
296
|
+
const ignoredKeys = [];
|
|
297
|
+
for (const [key, value] of Object.entries(projectConfig)) {
|
|
298
|
+
if (value === undefined) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (isProjectKeyHonored(key, trusted)) {
|
|
302
|
+
effective[key] = value;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
ignoredKeys.push(key);
|
|
306
|
+
}
|
|
307
|
+
return { effective, ignoredKeys: ignoredKeys.sort() };
|
|
308
|
+
}
|
|
273
309
|
export function resolveEffectiveConfig(options) {
|
|
274
310
|
const home = options.homeDir ?? homedir();
|
|
275
311
|
const scanTarget = resolve(options.scanTarget);
|
|
276
312
|
const globalConfigPath = options.cli?.configPath ?? join(home, ".codegate", "config.json");
|
|
277
313
|
const globalConfig = readConfigFile(globalConfigPath);
|
|
278
|
-
const
|
|
314
|
+
const rawProjectConfig = readConfigFile(join(scanTarget, ".codegate.json"));
|
|
315
|
+
const projectConfigTrusted = isTrustedDirectory(scanTarget, unique([DEFAULT_CONFIG.trusted_directories, globalConfig.trusted_directories]));
|
|
316
|
+
const { effective: projectConfig, ignoredKeys: ignoredProjectSettings } = partitionProjectConfig(rawProjectConfig, projectConfigTrusted);
|
|
279
317
|
const severity_threshold = pickFirst(normalizeSeverityThreshold(undefined), normalizeSeverityThreshold(projectConfig.severity_threshold), normalizeSeverityThreshold(globalConfig.severity_threshold), DEFAULT_CONFIG.severity_threshold) ?? DEFAULT_CONFIG.severity_threshold;
|
|
280
318
|
const output_format = pickFirst(options.cli?.format, normalizeOutputFormat(projectConfig.output_format), normalizeOutputFormat(globalConfig.output_format), DEFAULT_CONFIG.output_format) ?? DEFAULT_CONFIG.output_format;
|
|
281
319
|
return {
|
|
@@ -336,6 +374,7 @@ export function resolveEffectiveConfig(options) {
|
|
|
336
374
|
]),
|
|
337
375
|
unicode_analysis: pickFirst(projectConfig.unicode_analysis, globalConfig.unicode_analysis, DEFAULT_CONFIG.unicode_analysis) ?? DEFAULT_CONFIG.unicode_analysis,
|
|
338
376
|
check_ide_settings: pickFirst(projectConfig.check_ide_settings, globalConfig.check_ide_settings, DEFAULT_CONFIG.check_ide_settings) ?? DEFAULT_CONFIG.check_ide_settings,
|
|
377
|
+
first_scan_review: pickFirst(projectConfig.first_scan_review, globalConfig.first_scan_review, DEFAULT_CONFIG.first_scan_review) ?? true,
|
|
339
378
|
owasp_mapping: pickFirst(projectConfig.owasp_mapping, globalConfig.owasp_mapping, DEFAULT_CONFIG.owasp_mapping) ?? DEFAULT_CONFIG.owasp_mapping,
|
|
340
379
|
trusted_api_domains: unique([
|
|
341
380
|
DEFAULT_CONFIG.trusted_api_domains,
|
|
@@ -366,6 +405,9 @@ export function resolveEffectiveConfig(options) {
|
|
|
366
405
|
workflow_audits: {
|
|
367
406
|
enabled: pickFirst(projectConfig.workflow_audits?.enabled, globalConfig.workflow_audits?.enabled, DEFAULT_CONFIG.workflow_audits?.enabled) ?? false,
|
|
368
407
|
},
|
|
408
|
+
registry_heuristics: {
|
|
409
|
+
recent_publish_days: pickFirst(projectConfig.registry_heuristics?.recent_publish_days, globalConfig.registry_heuristics?.recent_publish_days, DEFAULT_CONFIG.registry_heuristics?.recent_publish_days) ?? 30,
|
|
410
|
+
},
|
|
369
411
|
suppress_findings: unique([
|
|
370
412
|
DEFAULT_CONFIG.suppress_findings,
|
|
371
413
|
globalConfig.suppress_findings,
|
|
@@ -378,6 +420,8 @@ export function resolveEffectiveConfig(options) {
|
|
|
378
420
|
],
|
|
379
421
|
layer3_remote_fetch_timeout_ms: pickFirst(readEnvOverride(LAYER3_REMOTE_FETCH_TIMEOUT_ENV), normalizePositiveInteger(projectConfig.layer3_remote_fetch_timeout_ms), normalizePositiveInteger(globalConfig.layer3_remote_fetch_timeout_ms), DEFAULT_CONFIG.layer3_remote_fetch_timeout_ms) ?? DEFAULT_CONFIG.layer3_remote_fetch_timeout_ms,
|
|
380
422
|
layer3_remote_fetch_max_bytes: pickFirst(readEnvOverride(LAYER3_REMOTE_FETCH_MAX_BYTES_ENV), normalizePositiveInteger(projectConfig.layer3_remote_fetch_max_bytes), normalizePositiveInteger(globalConfig.layer3_remote_fetch_max_bytes), DEFAULT_CONFIG.layer3_remote_fetch_max_bytes) ?? DEFAULT_CONFIG.layer3_remote_fetch_max_bytes,
|
|
423
|
+
project_config_trusted: projectConfigTrusted,
|
|
424
|
+
ignored_project_settings: ignoredProjectSettings.length > 0 ? ignoredProjectSettings : undefined,
|
|
381
425
|
};
|
|
382
426
|
}
|
|
383
427
|
export function computeExitCode(findings, threshold) {
|