codegate-ai 0.16.2 → 1.0.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 +13 -12
- 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/README.md
CHANGED
|
@@ -104,18 +104,19 @@ See the [Configuration](#configuration) section for full settings and examples.
|
|
|
104
104
|
|
|
105
105
|
## Core Commands
|
|
106
106
|
|
|
107
|
-
| Command | Purpose
|
|
108
|
-
| ------------------------ |
|
|
109
|
-
| `codegate scan [target]` | Scan a directory, file, or URL target for AI tool config risks.
|
|
110
|
-
| `codegate scan-content` | Scan inline JSON, YAML, TOML, Markdown, or text content.
|
|
111
|
-
| `codegate run <tool>` | Scan current directory, then launch selected AI tool if policy allows.
|
|
112
|
-
| `codegate skills [...]` | Wrap `npx skills` and preflight-scan `skills add` targets.
|
|
113
|
-
| `codegate clawhub [...]` | Wrap `npx clawhub` and preflight-scan `clawhub install` targets.
|
|
114
|
-
| `codegate undo [dir]` | Restore the most recent remediation backup session. Defaults to `.`.
|
|
115
|
-
| `codegate init` | Create `~/.codegate/config.json` with defaults.
|
|
116
|
-
| `codegate
|
|
117
|
-
| `codegate update-
|
|
118
|
-
| `codegate
|
|
107
|
+
| Command | Purpose |
|
|
108
|
+
| ------------------------ | ----------------------------------------------------------------------------------- |
|
|
109
|
+
| `codegate scan [target]` | Scan a directory, file, or URL target for AI tool config risks. |
|
|
110
|
+
| `codegate scan-content` | Scan inline JSON, YAML, TOML, Markdown, or text content. |
|
|
111
|
+
| `codegate run <tool>` | Scan current directory, then launch selected AI tool if policy allows. |
|
|
112
|
+
| `codegate skills [...]` | Wrap `npx skills` and preflight-scan `skills add` targets. |
|
|
113
|
+
| `codegate clawhub [...]` | Wrap `npx clawhub` and preflight-scan `clawhub install` targets. |
|
|
114
|
+
| `codegate undo [dir]` | Restore the most recent remediation backup session. Defaults to `.`. |
|
|
115
|
+
| `codegate init` | Create `~/.codegate/config.json` with defaults. |
|
|
116
|
+
| `codegate trust [dir]` | Trust a directory so its `.codegate.json` policy keys are honored. |
|
|
117
|
+
| `codegate update-kb` | Fetch, verify, and install signed knowledge-base content (`--check`, `--rollback`). |
|
|
118
|
+
| `codegate update-rules` | Fetch, verify, and install signed detection content (`--check`, `--rollback`). |
|
|
119
|
+
| `codegate --help` | Show CLI usage. |
|
|
119
120
|
|
|
120
121
|
## `scan` Command Flags
|
|
121
122
|
|
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;
|