@graneth/mcp-server
Advanced tools
+110
-3
@@ -161,4 +161,14 @@ #!/usr/bin/env node | ||
| const isNewPackage = firstVersionTime ? Date.now() - firstVersionTime.getTime() < NEW_PACKAGE_WINDOW_MS : false; | ||
| return { exists: true, isNewPackage, publishedAt: firstVersionTime }; | ||
| return { exists: true, isNewPackage, publishedAt: firstVersionTime, ...parseNpmTrustSignals(data) }; | ||
| } | ||
| function parseNpmTrustSignals(data) { | ||
| const latest = data["dist-tags"]?.latest ? data.versions?.[data["dist-tags"].latest] : null; | ||
| const scripts = latest?.scripts ?? {}; | ||
| return { | ||
| hasInstallScripts: !!(scripts.preinstall || scripts.install || scripts.postinstall), | ||
| hasProvenance: !!latest?.dist?.attestations, | ||
| isDeprecated: typeof latest?.deprecated === "string" && latest.deprecated.length > 0, | ||
| hasRepository: !!data.repository?.url | ||
| }; | ||
| } | ||
| async function fetchPypi(pkg) { | ||
@@ -652,2 +662,73 @@ const url = `https://pypi.org/pypi/${encodeURIComponent(pkg)}/json`; | ||
| // ../core-checks/src/riskScore.ts | ||
| var LOW_ADOPTION = 1e3; | ||
| var VERY_LOW_ADOPTION = 50; | ||
| var HIGH_ADOPTION = 1e5; | ||
| var VERY_HIGH_ADOPTION = 1e6; | ||
| var ESTABLISHED_ADOPTION = 5e3; | ||
| function bandOf(score) { | ||
| if (score >= 75) return "critical"; | ||
| if (score >= 50) return "high"; | ||
| if (score >= 25) return "elevated"; | ||
| return "minimal"; | ||
| } | ||
| function isNew(s) { | ||
| return s.isNewPackage === true || s.ageDays != null && s.ageDays < 30; | ||
| } | ||
| function applyDominantSignals(s, add) { | ||
| if (s.isSecurityHolding) add("security_holding", 70, "Registry seized this name after a malware/typosquat incident."); | ||
| if (s.knownHallucination) add("known_hallucination", 45, "Name appears in the known-AI-hallucination corpus and has since been registered (slopsquat)."); | ||
| if (s.typosquatDistance === 1) add("typosquat_1", 55, "One edit away from a hugely popular package name (typosquat shape)."); | ||
| else if (s.typosquatDistance === 2) add("typosquat_2", 35, "Two edits away from a popular package name (possible typosquat)."); | ||
| } | ||
| function applyCompoundingSignals(s, add, newPkg, dl) { | ||
| const lowAdoption = dl != null && dl < LOW_ADOPTION; | ||
| const veryLowAdoption = dl != null && dl < VERY_LOW_ADOPTION; | ||
| if (newPkg) add("new_package", 22, "Published within the last 30 days \u2014 the pre-registration attack window."); | ||
| if (s.hasInstallScripts) { | ||
| if (newPkg || lowAdoption) add("install_script_risk", 30, "Runs an install script AND is new / near-zero adoption \u2014 the npm dropper shape."); | ||
| else add("install_script", 5, "Runs an install script (common for native builds; low risk on an established package)."); | ||
| } | ||
| if (s.isDeprecated) add("deprecated", 15, "Deprecated by its maintainer \u2014 unpatched and a name-takeover target."); | ||
| if (veryLowAdoption) add("very_low_adoption", 28, "Near-zero adoption \u2014 no crowd has vetted this dependency."); | ||
| else if (lowAdoption) add("low_adoption", 15, "Low adoption for its age."); | ||
| if (s.hasRepository === false) add("no_repository", 12, "No linked source repository to inspect."); | ||
| if (s.maintainersCount === 1) add("single_maintainer", 8, "Single maintainer \u2014 one account compromise ships to every consumer."); | ||
| } | ||
| function applyMitigators(s, add, newPkg, dl) { | ||
| if (s.hasProvenance) add("provenance", -25, "Registry-verified build provenance (public CI from a public repo) \u2014 strong legitimacy."); | ||
| if (dl != null && dl > VERY_HIGH_ADOPTION) add("very_high_adoption", -50, "Very high adoption \u2014 extensively used and watched."); | ||
| else if (dl != null && dl > HIGH_ADOPTION) add("high_adoption", -35, "High adoption \u2014 widely used."); | ||
| if (!newPkg && s.hasRepository === true && dl != null && dl >= ESTABLISHED_ADOPTION) { | ||
| add("established", -15, "Established: aged, repo-backed, with real adoption."); | ||
| } | ||
| } | ||
| function computeDependencyRisk(s) { | ||
| if (s.unreachable) return null; | ||
| if (!s.exists) { | ||
| return { | ||
| score: 100, | ||
| band: "critical", | ||
| factors: [{ signal: "nonexistent", points: 100, note: "Package does not exist in its registry (AI-hallucinated / ghost)." }] | ||
| }; | ||
| } | ||
| const factors = []; | ||
| const add = (signal, points, note) => { | ||
| factors.push({ signal, points, note }); | ||
| }; | ||
| const newPkg = isNew(s); | ||
| const dl = s.weeklyDownloads; | ||
| applyDominantSignals(s, add); | ||
| applyCompoundingSignals(s, add, newPkg, dl); | ||
| applyMitigators(s, add, newPkg, dl); | ||
| const raw = factors.reduce((sum, f) => sum + f.points, 0); | ||
| const score = Math.max(0, Math.min(100, raw)); | ||
| return { score, band: bandOf(score), factors }; | ||
| } | ||
| function isStackedRisk(risk) { | ||
| if (!risk || risk.band !== "high" && risk.band !== "critical") return false; | ||
| const maxPositive = Math.max(0, ...risk.factors.filter((f) => f.points > 0).map((f) => f.points)); | ||
| return maxPositive < 50; | ||
| } | ||
| // ../core-checks/src/preflight.ts | ||
@@ -816,2 +897,4 @@ var TEST_FIXTURE_PATH_RE = /(^|[\\/])(__tests__|__mocks__|fixtures?|testdata|tests?)[\\/]|\.(test|spec)\.[^\\/]+$|(^|[\\/])test_[^\\/]*\.py$|_test\.py$|(^|[\\/])conftest\.py$/i; | ||
| } | ||
| const riskFinding = dependencyRiskShapeFinding(ref, signal); | ||
| if (riskFinding) findings.push(riskFinding); | ||
| } | ||
@@ -821,2 +904,26 @@ } | ||
| } | ||
| function dependencyRiskShapeFinding(ref, signal) { | ||
| if (!signal.exists || signal.unreachable) return null; | ||
| const risk = computeDependencyRisk({ | ||
| exists: true, | ||
| isNewPackage: signal.isNewPackage, | ||
| ageDays: signal.publishedAt ? Math.floor((Date.now() - signal.publishedAt.getTime()) / 864e5) : null, | ||
| hasRepository: signal.hasRepository, | ||
| hasProvenance: signal.hasProvenance, | ||
| hasInstallScripts: signal.hasInstallScripts, | ||
| isDeprecated: signal.isDeprecated | ||
| }); | ||
| if (!isStackedRisk(risk)) return null; | ||
| const shape = risk.factors.filter((f) => f.points > 0).map((f) => f.note).join(" "); | ||
| return { | ||
| type: "dependency_risk_shape", | ||
| severity: "warning", | ||
| title: `"${ref.pkg}" has a compounded supply-chain risk shape (${risk.score}/100)`, | ||
| description: `"${ref.pkg}" exists, but several individually-weak signals compound into an elevated risk shape \u2014 the pattern of a low-adoption malicious package or patient squat that trips no single alarm. ${shape} Advisory assessment from package metadata, NOT a malware detection. Found in \`${ref.filename}\` at line ${ref.line}.`, | ||
| file: ref.filename, | ||
| line: ref.line, | ||
| recommendation: `Review "${ref.pkg}" before trusting it \u2014 inspect its repository, maintainer history and recent releases. If your agent chose it and you can't justify it, prefer a well-established alternative.`, | ||
| cve: "CWE-1357" | ||
| }; | ||
| } | ||
| async function preFlightCheck(files) { | ||
@@ -843,4 +950,4 @@ const [secretFindings, packageFindings] = await Promise.all([ | ||
| // src/index.ts | ||
| var VERSION = "0.4.1"; | ||
| var PRE_FLIGHT_CHECK_DESCRIPTION = "Security pre-flight check for local file changes BEFORE committing. Run this whenever you are about to suggest `git commit`, `git push`, or open a pull request \u2014 especially when changes add package imports or dependencies (package.json / requirements.txt / Cargo.toml / go.mod / Gemfile / composer.json / import statements) or could contain secrets. Detects AI-hallucinated (non-existent) packages by live-checking six registries (npm, PyPI, crates.io, RubyGems, Go module proxy, Packagist), plus hardcoded credentials via pattern + entropy analysis. Always free, no account required. Returns CLEAR, REVIEW_REQUIRED, or BLOCKED with specific findings and remediation."; | ||
| var VERSION = "0.5.0"; | ||
| var PRE_FLIGHT_CHECK_DESCRIPTION = "Security pre-flight check for local file changes BEFORE committing. Run this whenever you are about to suggest `git commit`, `git push`, or open a pull request \u2014 especially when changes add package imports or dependencies (package.json / requirements.txt / Cargo.toml / go.mod / Gemfile / composer.json / import statements) or could contain secrets. Detects AI-hallucinated (non-existent) packages by live-checking six registries (npm, PyPI, crates.io, RubyGems, Go module proxy, Packagist); risk-scores dependencies an AI agent introduced by compounding metadata signals (new + install-scripts + low-adoption + no-provenance = an attack shape no single check flags); and finds hardcoded credentials via pattern + entropy analysis. Always free, no account required. Returns CLEAR, REVIEW_REQUIRED, or BLOCKED with specific findings and remediation."; | ||
| var PRE_FLIGHT_CHECK_SCHEMA = { | ||
@@ -847,0 +954,0 @@ files: z.array( |
+2
-2
| { | ||
| "name": "@graneth/mcp-server", | ||
| "version": "0.4.1", | ||
| "description": "Account-free MCP server: catch AI-hallucinated packages (npm, PyPI, crates.io, RubyGems, Go, Packagist) and hardcoded secrets before you commit. Exposes the free pre_flight_check tool over stdio.", | ||
| "version": "0.5.0", | ||
| "description": "Account-free MCP server: catch AI-hallucinated packages (npm, PyPI, crates.io, RubyGems, Go, Packagist), risk-score the dependencies an AI agent introduces, and find hardcoded secrets before you commit. Exposes the free pre_flight_check tool over stdio.", | ||
| "type": "module", | ||
@@ -6,0 +6,0 @@ "bin": { |
+7
-0
@@ -24,2 +24,9 @@ # @graneth/mcp-server | ||
| slopsquatting attack window. → **REVIEW_REQUIRED**. | ||
| - **Risk-score the dependencies your AI agent introduced** — existence is only | ||
| the start. A *real* package can still carry the shape attackers exploit: | ||
| brand-new **and** running an install script **and** near-zero adoption **and** | ||
| no provenance attestation. None of those trips a single alarm; compounded they | ||
| do. An advisory `dependency_risk_shape` warning surfaces that stacked shape at | ||
| generation time — explicitly a risk **assessment**, never a malware claim. | ||
| → **REVIEW_REQUIRED**. | ||
| - **Catch hardcoded secrets** — known credential patterns (AWS, GitHub, Slack, | ||
@@ -26,0 +33,0 @@ OpenAI `sk-…`/`sk-proj-…`, Anthropic `sk-ant-…`, Stripe `sk_live_…`, PEM |
49538
15.17%1019
11.86%91
8.33%