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.
Files changed (75) hide show
  1. package/README.md +13 -12
  2. package/dist/cli.d.ts +3 -1
  3. package/dist/cli.js +153 -44
  4. package/dist/commands/scan-command.d.ts +2 -1
  5. package/dist/commands/scan-command.js +6 -1
  6. package/dist/commands/trust.d.ts +28 -0
  7. package/dist/commands/trust.js +69 -0
  8. package/dist/config/inline-ignore.d.ts +10 -2
  9. package/dist/config/inline-ignore.js +5 -1
  10. package/dist/config/suppression-policy.d.ts +1 -1
  11. package/dist/config/suppression-policy.js +4 -1
  12. package/dist/config/trust.d.ts +2 -0
  13. package/dist/config/trust.js +19 -0
  14. package/dist/config.d.ts +14 -0
  15. package/dist/config.js +45 -1
  16. package/dist/content/content-bundle.d.ts +27 -0
  17. package/dist/content/content-bundle.js +73 -0
  18. package/dist/content/content-store.d.ts +27 -0
  19. package/dist/content/content-store.js +0 -0
  20. package/dist/content/content-updater.d.ts +32 -0
  21. package/dist/content/content-updater.js +111 -0
  22. package/dist/content/known-bad.d.ts +26 -0
  23. package/dist/content/known-bad.js +100 -0
  24. package/dist/content/publisher-key.d.ts +10 -0
  25. package/dist/content/publisher-key.js +10 -0
  26. package/dist/layer1-discovery/knowledge-base.js +33 -1
  27. package/dist/layer2-static/data/popular-mcp-packages.d.ts +16 -0
  28. package/dist/layer2-static/data/popular-mcp-packages.js +83 -0
  29. package/dist/layer2-static/detectors/known-bad.d.ts +22 -0
  30. package/dist/layer2-static/detectors/known-bad.js +116 -0
  31. package/dist/layer2-static/detectors/mcp-package-hygiene.d.ts +28 -0
  32. package/dist/layer2-static/detectors/mcp-package-hygiene.js +191 -0
  33. package/dist/layer2-static/detectors/rule-file.js +128 -75
  34. package/dist/layer2-static/detectors/skill-frontmatter.d.ts +6 -0
  35. package/dist/layer2-static/detectors/skill-frontmatter.js +130 -0
  36. package/dist/layer2-static/engine.d.ts +2 -0
  37. package/dist/layer2-static/engine.js +0 -0
  38. package/dist/layer2-static/rule-pack-loader.js +24 -1
  39. package/dist/layer2-static/state/scan-state.d.ts +7 -2
  40. package/dist/layer2-static/state/scan-state.js +74 -30
  41. package/dist/layer2-static/text/confusables.d.ts +7 -0
  42. package/dist/layer2-static/text/confusables.js +70 -0
  43. package/dist/layer2-static/text/edit-distance.d.ts +6 -0
  44. package/dist/layer2-static/text/edit-distance.js +33 -0
  45. package/dist/layer2-static/text/encoded-payloads.d.ts +16 -0
  46. package/dist/layer2-static/text/encoded-payloads.js +116 -0
  47. package/dist/layer2-static/text/normalize.d.ts +11 -0
  48. package/dist/layer2-static/text/normalize.js +19 -0
  49. package/dist/layer2-static/text/override-phrases.d.ts +14 -0
  50. package/dist/layer2-static/text/override-phrases.js +49 -0
  51. package/dist/layer2-static/text/threat-patterns.d.ts +33 -0
  52. package/dist/layer2-static/text/threat-patterns.js +45 -0
  53. package/dist/layer2-static/text/unicode.d.ts +33 -0
  54. package/dist/layer2-static/text/unicode.js +83 -0
  55. package/dist/layer3-dynamic/deep-resource-executor.d.ts +21 -0
  56. package/dist/layer3-dynamic/deep-resource-executor.js +73 -0
  57. package/dist/layer3-dynamic/meta-agent.js +2 -1
  58. package/dist/layer3-dynamic/registry-client.d.ts +26 -0
  59. package/dist/layer3-dynamic/registry-client.js +138 -0
  60. package/dist/layer3-dynamic/registry-findings.d.ts +7 -0
  61. package/dist/layer3-dynamic/registry-findings.js +64 -0
  62. package/dist/layer3-dynamic/tool-description-scanner.js +22 -13
  63. package/dist/layer3-dynamic/toxic-flow.d.ts +4 -0
  64. package/dist/layer3-dynamic/toxic-flow.js +41 -8
  65. package/dist/pipeline.d.ts +5 -2
  66. package/dist/pipeline.js +53 -16
  67. package/dist/report-summary.d.ts +1 -1
  68. package/dist/report-summary.js +9 -1
  69. package/dist/scan.d.ts +13 -0
  70. package/dist/scan.js +291 -17
  71. package/dist/types/finding.d.ts +14 -0
  72. package/dist/types/finding.js +14 -0
  73. package/dist/types/report.d.ts +2 -0
  74. package/dist/wrapper.js +2 -19
  75. package/package.json +1 -1
@@ -0,0 +1,83 @@
1
+ import { loadActiveContentBundle } from "../../content/content-store.js";
2
+ /**
3
+ * Well-known MCP server packages used as the reference set for typosquat
4
+ * detection. Curated, not exhaustive: entries should be packages an
5
+ * attacker would plausibly imitate. Data-driven so the content feed can
6
+ * extend it without a release.
7
+ */
8
+ export const POPULAR_MCP_PACKAGES = {
9
+ npm: [
10
+ "@modelcontextprotocol/server-brave-search",
11
+ "@modelcontextprotocol/server-everything",
12
+ "@modelcontextprotocol/server-filesystem",
13
+ "@modelcontextprotocol/server-github",
14
+ "@modelcontextprotocol/server-gitlab",
15
+ "@modelcontextprotocol/server-google-maps",
16
+ "@modelcontextprotocol/server-memory",
17
+ "@modelcontextprotocol/server-postgres",
18
+ "@modelcontextprotocol/server-puppeteer",
19
+ "@modelcontextprotocol/server-sequential-thinking",
20
+ "@modelcontextprotocol/server-slack",
21
+ "@modelcontextprotocol/server-sqlite",
22
+ "@modelcontextprotocol/inspector",
23
+ "@anthropic/mcp-server-filesystem",
24
+ "@playwright/mcp",
25
+ "@browserbasehq/mcp",
26
+ "@notionhq/notion-mcp-server",
27
+ "@supabase/mcp-server-supabase",
28
+ "@upstash/context7-mcp",
29
+ "@21st-dev/magic",
30
+ "@sentry/mcp-server",
31
+ "@elastic/mcp-server-elasticsearch",
32
+ "firecrawl-mcp",
33
+ "tavily-mcp",
34
+ "exa-mcp-server",
35
+ "mcp-remote",
36
+ "supergateway",
37
+ "figma-developer-mcp",
38
+ "chrome-devtools-mcp",
39
+ "graphlit-mcp-server",
40
+ ],
41
+ pypi: [
42
+ "mcp",
43
+ "mcp-server-git",
44
+ "mcp-server-fetch",
45
+ "mcp-server-time",
46
+ "mcp-server-sqlite",
47
+ "mcp-server-sentry",
48
+ "fastmcp",
49
+ "mcp-atlassian",
50
+ "awslabs.aws-documentation-mcp-server",
51
+ ],
52
+ };
53
+ let cachedActivePackages = null;
54
+ export function resetActivePopularMcpPackagesCache() {
55
+ cachedActivePackages = null;
56
+ }
57
+ function feedList(value) {
58
+ return Array.isArray(value)
59
+ ? value.filter((entry) => typeof entry === "string" && entry.length > 0)
60
+ : [];
61
+ }
62
+ /** Bundled popular packages plus any extras delivered by the verified content feed. */
63
+ export function activePopularMcpPackages() {
64
+ if (cachedActivePackages) {
65
+ return cachedActivePackages;
66
+ }
67
+ let npmExtra;
68
+ let pypiExtra;
69
+ try {
70
+ const bundle = loadActiveContentBundle();
71
+ npmExtra = feedList(bundle?.popular_packages?.npm);
72
+ pypiExtra = feedList(bundle?.popular_packages?.pypi);
73
+ }
74
+ catch {
75
+ npmExtra = [];
76
+ pypiExtra = [];
77
+ }
78
+ cachedActivePackages = {
79
+ npm: [...new Set([...POPULAR_MCP_PACKAGES.npm, ...npmExtra])],
80
+ pypi: [...new Set([...POPULAR_MCP_PACKAGES.pypi, ...pypiExtra])],
81
+ };
82
+ return cachedActivePackages;
83
+ }
@@ -0,0 +1,22 @@
1
+ import { type ResolvedKnownBadIndicators } from "../../content/known-bad.js";
2
+ import type { Finding } from "../../types/finding.js";
3
+ export interface KnownBadDetectionInput {
4
+ filePath: string;
5
+ parsed: unknown;
6
+ textContent: string;
7
+ indicators: ResolvedKnownBadIndicators;
8
+ }
9
+ export declare const KNOWN_BAD_MATCH_KIND: {
10
+ readonly FileHash: "file-hash";
11
+ readonly PackageName: "package-name";
12
+ readonly UrlPattern: "url-pattern";
13
+ };
14
+ export type KnownBadMatchKind = (typeof KNOWN_BAD_MATCH_KIND)[keyof typeof KNOWN_BAD_MATCH_KIND];
15
+ export declare const KNOWN_BAD_RULE_ID = "known-malicious-content";
16
+ export declare function detectKnownBadContent(input: KnownBadDetectionInput): Finding[];
17
+ /**
18
+ * Escalate findings whose stable fingerprint appears in the known-bad
19
+ * indicator set to CRITICAL. Runs on the assembled finding list so it also
20
+ * covers findings produced outside the static engine.
21
+ */
22
+ export declare function escalateKnownBadFindings(findings: Finding[], indicators: ResolvedKnownBadIndicators): Finding[];
@@ -0,0 +1,116 @@
1
+ import { createHash } from "node:crypto";
2
+ import { hasKnownBadIndicators } from "../../content/known-bad.js";
3
+ import { buildFindingFingerprint } from "../../report/finding-fingerprint.js";
4
+ import { collectMcpPackageLaunches, parsePackageLocator } from "./mcp-package-hygiene.js";
5
+ export const KNOWN_BAD_MATCH_KIND = {
6
+ FileHash: "file-hash",
7
+ PackageName: "package-name",
8
+ UrlPattern: "url-pattern",
9
+ };
10
+ export const KNOWN_BAD_RULE_ID = "known-malicious-content";
11
+ const URL_PATTERN = /https?:\/\/[^\s"'`<>)\]}]+/giu;
12
+ const MATCH_KIND_CATEGORY = {
13
+ [KNOWN_BAD_MATCH_KIND.FileHash]: "CONFIG_PRESENT",
14
+ [KNOWN_BAD_MATCH_KIND.PackageName]: "COMMAND_EXEC",
15
+ [KNOWN_BAD_MATCH_KIND.UrlPattern]: "CONFIG_PRESENT",
16
+ };
17
+ function makeKnownBadFinding(input, kind, field, description, evidence) {
18
+ return {
19
+ rule_id: KNOWN_BAD_RULE_ID,
20
+ finding_id: `KNOWN_MALICIOUS_CONTENT-${kind}-${input.filePath}-${field}-${evidence}`,
21
+ severity: "CRITICAL",
22
+ category: MATCH_KIND_CATEGORY[kind],
23
+ layer: "L2",
24
+ file_path: input.filePath,
25
+ location: { field },
26
+ description,
27
+ affected_tools: ["claude-code", "codex-cli", "opencode", "cursor", "windsurf"],
28
+ cve: null,
29
+ owasp: ["ASI05"],
30
+ cwe: "CWE-506",
31
+ confidence: "HIGH",
32
+ fixable: true,
33
+ remediation_actions: ["quarantine_file", "remove_field"],
34
+ metadata: {
35
+ sources: [input.filePath, field],
36
+ risk_tags: ["known-bad", kind],
37
+ origin: "known-bad",
38
+ },
39
+ evidence,
40
+ suppressed: false,
41
+ };
42
+ }
43
+ function detectFileHashMatch(input) {
44
+ const hash = createHash("sha256").update(input.textContent, "utf8").digest("hex");
45
+ if (!input.indicators.fileSha256.has(hash)) {
46
+ return [];
47
+ }
48
+ return [
49
+ makeKnownBadFinding(input, KNOWN_BAD_MATCH_KIND.FileHash, "content", `File content matches a known-malicious SHA-256 indicator (${hash}). ` +
50
+ "Quarantine this file and audit how it got here.", `sha256:${hash}`),
51
+ ];
52
+ }
53
+ function detectPackageNameMatches(input) {
54
+ const findings = [];
55
+ for (const launch of collectMcpPackageLaunches(input.parsed)) {
56
+ const { name } = parsePackageLocator(launch.registry, launch.locator);
57
+ if (!input.indicators.packageNames.has(name.toLowerCase())) {
58
+ continue;
59
+ }
60
+ findings.push(makeKnownBadFinding(input, KNOWN_BAD_MATCH_KIND.PackageName, launch.serverPath, `MCP server runs ${launch.registry} package "${name}", which is a known-malicious package. ` +
61
+ "Remove this server configuration immediately.", `${launch.serverPath}: ${launch.locator}`));
62
+ }
63
+ return findings;
64
+ }
65
+ function detectUrlPatternMatches(input) {
66
+ const findings = [];
67
+ const reported = new Set();
68
+ for (const match of input.textContent.matchAll(URL_PATTERN)) {
69
+ const url = match[0];
70
+ const normalized = url.toLowerCase();
71
+ const pattern = input.indicators.urlPatterns.find((entry) => normalized.includes(entry));
72
+ if (!pattern || reported.has(normalized)) {
73
+ continue;
74
+ }
75
+ reported.add(normalized);
76
+ findings.push(makeKnownBadFinding(input, KNOWN_BAD_MATCH_KIND.UrlPattern, "content", `File references "${url}", which matches the known-malicious URL indicator "${pattern}". ` +
77
+ "Remove the reference and audit anything fetched from it.", url));
78
+ }
79
+ return findings;
80
+ }
81
+ export function detectKnownBadContent(input) {
82
+ if (!hasKnownBadIndicators(input.indicators)) {
83
+ return [];
84
+ }
85
+ return [
86
+ ...detectFileHashMatch(input),
87
+ ...detectPackageNameMatches(input),
88
+ ...detectUrlPatternMatches(input),
89
+ ];
90
+ }
91
+ /**
92
+ * Escalate findings whose stable fingerprint appears in the known-bad
93
+ * indicator set to CRITICAL. Runs on the assembled finding list so it also
94
+ * covers findings produced outside the static engine.
95
+ */
96
+ export function escalateKnownBadFindings(findings, indicators) {
97
+ if (indicators.findingFingerprints.size === 0) {
98
+ return findings;
99
+ }
100
+ return findings.map((finding) => {
101
+ const fingerprint = finding.fingerprint ?? buildFindingFingerprint(finding);
102
+ if (!indicators.findingFingerprints.has(fingerprint)) {
103
+ return finding;
104
+ }
105
+ return {
106
+ ...finding,
107
+ severity: "CRITICAL",
108
+ description: `${finding.description} This finding matches a known-malicious indicator ` +
109
+ "and has been escalated to CRITICAL.",
110
+ metadata: {
111
+ ...finding.metadata,
112
+ risk_tags: [...(finding.metadata?.risk_tags ?? []), "known-bad"],
113
+ },
114
+ };
115
+ });
116
+ }
@@ -0,0 +1,28 @@
1
+ import type { Finding } from "../../types/finding.js";
2
+ export interface McpPackageHygieneInput {
3
+ filePath: string;
4
+ parsed: unknown;
5
+ knownSafeMcpServers: string[];
6
+ }
7
+ export declare const PACKAGE_REGISTRY: {
8
+ readonly Npm: "npm";
9
+ readonly Pypi: "pypi";
10
+ };
11
+ export type PackageRegistry = (typeof PACKAGE_REGISTRY)[keyof typeof PACKAGE_REGISTRY];
12
+ export interface PackageLaunch {
13
+ registry: PackageRegistry;
14
+ locator: string;
15
+ serverPath: string;
16
+ autoConfirm: boolean;
17
+ }
18
+ interface ParsedLocator {
19
+ name: string;
20
+ version: string | null;
21
+ }
22
+ export declare function parsePackageLocator(registry: PackageRegistry, locator: string): ParsedLocator;
23
+ export declare function isPinnedLocator(registry: PackageRegistry, locator: string): boolean;
24
+ export declare function findLikelyTyposquatTarget(registry: PackageRegistry, name: string): string | null;
25
+ /** MCP server package launches (npx/uvx/pipx style) found in a parsed config. */
26
+ export declare function collectMcpPackageLaunches(parsed: unknown): PackageLaunch[];
27
+ export declare function detectMcpPackageHygiene(input: McpPackageHygieneInput): Finding[];
28
+ export {};
@@ -0,0 +1,191 @@
1
+ import { activePopularMcpPackages } from "../data/popular-mcp-packages.js";
2
+ import { damerauLevenshtein } from "../text/edit-distance.js";
3
+ const MCP_SERVER_CONTAINER_KEYS = ["mcpServers", "mcp_servers", "context_servers"];
4
+ export const PACKAGE_REGISTRY = {
5
+ Npm: "npm",
6
+ Pypi: "pypi",
7
+ };
8
+ const NPM_LAUNCHERS = new Set(["npx", "pnpx", "bunx"]);
9
+ const PYPI_LAUNCHERS = new Set(["uvx", "pipx"]);
10
+ // Exact versions pin the executed code; tags and ranges resolve at launch time.
11
+ const EXACT_NPM_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[\w.]+)?$/u;
12
+ const EXACT_PYPI_VERSION_PATTERN = /^==\s*\d+(?:\.\d+)*(?:[a-z0-9.]*)$/iu;
13
+ const MIN_TYPOSQUAT_NAME_LENGTH = 4;
14
+ function isRecord(value) {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16
+ }
17
+ function parseNpmLocator(locator) {
18
+ const versionIndex = locator.lastIndexOf("@");
19
+ if (versionIndex > 0) {
20
+ return { name: locator.slice(0, versionIndex), version: locator.slice(versionIndex + 1) };
21
+ }
22
+ return { name: locator, version: null };
23
+ }
24
+ function parsePypiLocator(locator) {
25
+ const specIndex = locator.search(/[=<>!~]/u);
26
+ if (specIndex > 0) {
27
+ return { name: locator.slice(0, specIndex), version: locator.slice(specIndex) };
28
+ }
29
+ return { name: locator, version: null };
30
+ }
31
+ export function parsePackageLocator(registry, locator) {
32
+ return registry === PACKAGE_REGISTRY.Npm ? parseNpmLocator(locator) : parsePypiLocator(locator);
33
+ }
34
+ export function isPinnedLocator(registry, locator) {
35
+ const { version } = parsePackageLocator(registry, locator);
36
+ if (version === null) {
37
+ return false;
38
+ }
39
+ return registry === PACKAGE_REGISTRY.Npm
40
+ ? EXACT_NPM_VERSION_PATTERN.test(version)
41
+ : EXACT_PYPI_VERSION_PATTERN.test(version);
42
+ }
43
+ function typosquatThreshold(name) {
44
+ return name.length >= 10 ? 2 : 1;
45
+ }
46
+ export function findLikelyTyposquatTarget(registry, name) {
47
+ const popular = activePopularMcpPackages()[registry];
48
+ const normalized = name.toLowerCase();
49
+ if (normalized.length < MIN_TYPOSQUAT_NAME_LENGTH || popular.includes(normalized)) {
50
+ return null;
51
+ }
52
+ let best = null;
53
+ for (const candidate of popular) {
54
+ const distance = damerauLevenshtein(normalized, candidate);
55
+ if (distance > 0 && distance <= typosquatThreshold(normalized)) {
56
+ if (!best || distance < best.distance) {
57
+ best = { target: candidate, distance };
58
+ }
59
+ }
60
+ }
61
+ return best?.target ?? null;
62
+ }
63
+ function commandTokens(config) {
64
+ if (Array.isArray(config.command) && config.command.every((entry) => typeof entry === "string")) {
65
+ return config.command;
66
+ }
67
+ if (typeof config.command === "string") {
68
+ const args = Array.isArray(config.args)
69
+ ? config.args.filter((entry) => typeof entry === "string")
70
+ : [];
71
+ return [config.command, ...args];
72
+ }
73
+ return [];
74
+ }
75
+ function firstNonFlag(tokens, startIndex) {
76
+ for (let index = startIndex; index < tokens.length; index += 1) {
77
+ const token = tokens[index];
78
+ if (typeof token !== "string" || token.length === 0 || token.startsWith("-")) {
79
+ continue;
80
+ }
81
+ return token;
82
+ }
83
+ return null;
84
+ }
85
+ function hasAutoConfirmFlag(tokens) {
86
+ return tokens.some((token) => token === "-y" || token === "--yes");
87
+ }
88
+ function collectPackageLaunches(value, path, launches) {
89
+ if (!isRecord(value)) {
90
+ return;
91
+ }
92
+ for (const containerKey of MCP_SERVER_CONTAINER_KEYS) {
93
+ const container = value[containerKey];
94
+ if (!isRecord(container)) {
95
+ continue;
96
+ }
97
+ for (const [serverName, serverConfig] of Object.entries(container)) {
98
+ if (!isRecord(serverConfig)) {
99
+ continue;
100
+ }
101
+ const tokens = commandTokens(serverConfig);
102
+ if (tokens.length === 0) {
103
+ continue;
104
+ }
105
+ const launcher = (tokens[0] ?? "").toLowerCase();
106
+ const registry = NPM_LAUNCHERS.has(launcher)
107
+ ? PACKAGE_REGISTRY.Npm
108
+ : PYPI_LAUNCHERS.has(launcher)
109
+ ? PACKAGE_REGISTRY.Pypi
110
+ : null;
111
+ if (!registry) {
112
+ continue;
113
+ }
114
+ const locator = firstNonFlag(tokens, 1);
115
+ if (!locator) {
116
+ continue;
117
+ }
118
+ launches.push({
119
+ registry,
120
+ locator,
121
+ serverPath: `${path}${containerKey}.${serverName}`,
122
+ autoConfirm: hasAutoConfirmFlag(tokens),
123
+ });
124
+ }
125
+ }
126
+ for (const [key, nested] of Object.entries(value)) {
127
+ if (MCP_SERVER_CONTAINER_KEYS.includes(key)) {
128
+ continue;
129
+ }
130
+ collectPackageLaunches(nested, `${path}${key}.`, launches);
131
+ }
132
+ }
133
+ function makeFinding(input, launch, ruleId, severity, description) {
134
+ return {
135
+ rule_id: ruleId,
136
+ finding_id: `${ruleId.toUpperCase().replaceAll("-", "_")}-${input.filePath}-${launch.serverPath}`,
137
+ severity,
138
+ category: "COMMAND_EXEC",
139
+ layer: "L2",
140
+ file_path: input.filePath,
141
+ location: { field: launch.serverPath },
142
+ description,
143
+ affected_tools: ["claude-code", "codex-cli", "opencode", "cursor", "windsurf"],
144
+ cve: null,
145
+ owasp: ["ASI05"],
146
+ cwe: "CWE-829",
147
+ confidence: "HIGH",
148
+ fixable: true,
149
+ remediation_actions: ["remove_field", "replace_with_default"],
150
+ metadata: {
151
+ sources: [input.filePath, launch.serverPath],
152
+ risk_tags: ["mcp", "supply-chain"],
153
+ origin: "mcp-package-hygiene",
154
+ },
155
+ evidence: `${launch.serverPath}: ${launch.locator}`,
156
+ suppressed: false,
157
+ };
158
+ }
159
+ /** MCP server package launches (npx/uvx/pipx style) found in a parsed config. */
160
+ export function collectMcpPackageLaunches(parsed) {
161
+ const launches = [];
162
+ collectPackageLaunches(parsed, "", launches);
163
+ return launches;
164
+ }
165
+ export function detectMcpPackageHygiene(input) {
166
+ const launches = collectMcpPackageLaunches(input.parsed);
167
+ if (launches.length === 0) {
168
+ return [];
169
+ }
170
+ const knownSafe = new Set(input.knownSafeMcpServers.map((entry) => entry.toLowerCase()));
171
+ const findings = [];
172
+ for (const launch of launches) {
173
+ const { name } = parsePackageLocator(launch.registry, launch.locator);
174
+ if (knownSafe.has(name.toLowerCase())) {
175
+ continue;
176
+ }
177
+ if (!isPinnedLocator(launch.registry, launch.locator)) {
178
+ const autoConfirmNote = launch.autoConfirm
179
+ ? " The launcher auto-confirms installation (-y), so new versions run without any prompt."
180
+ : "";
181
+ findings.push(makeFinding(input, launch, "mcp-unpinned-package", "MEDIUM", `MCP server runs ${launch.registry} package "${launch.locator}" without an exact version pin, ` +
182
+ `so it executes whatever the registry serves at launch time.${autoConfirmNote}`));
183
+ }
184
+ const typosquatTarget = findLikelyTyposquatTarget(launch.registry, name);
185
+ if (typosquatTarget) {
186
+ findings.push(makeFinding(input, launch, "mcp-possible-typosquat", "HIGH", `MCP server package "${name}" is one edit away from the well-known package ` +
187
+ `"${typosquatTarget}". Verify this is the intended package and not a typosquat.`));
188
+ }
189
+ }
190
+ return findings;
191
+ }