Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -63,27 +63,28 @@ GITTENSORY_REVIEW_ENRICHMENT=false
# # Unknown names warn and are ignored; a typo-only list runs no analyzers.
# BEGIN GENERATED REES ANALYZERS
# Current analyzer names:
# dependency,lockfileDrift,secret,license,installScript,heavyDependency,hardcodedUrl,actionPin
# eol,redos,provenance,codeowners,secretLog,assetWeight,typosquat,commitSignature,iacMisconfig
# nativeBuild,history,docCommentDrift,duplication,churnHotspot,blameLink,approvalIntegrity
# ciCheckSignals,undocumentedExport,staleBranch,commitHygiene,pendingReviewRequests,testRatio
# migrationSafety,looseRange,terminology,todoMarker,magicNumber,conflictMarker,debugLeftover
# commitLint
#
# Profile defaults:
# fast: dependency,lockfileDrift,secret,license,installScript,heavyDependency,hardcodedUrl
# actionPin,eol,redos,provenance,secretLog,typosquat,iacMisconfig,nativeBuild,testRatio
# migrationSafety,looseRange,terminology,todoMarker,magicNumber,conflictMarker,debugLeftover
# balanced (default): dependency,lockfileDrift,secret,license,installScript,heavyDependency
# dependency,dependencyDiff,lockfileDrift,secret,license,installScript,heavyDependency
# hardcodedUrl,actionPin,eol,redos,provenance,codeowners,secretLog,assetWeight,typosquat
# commitSignature,iacMisconfig,nativeBuild,history,docCommentDrift,duplication,churnHotspot
# blameLink,approvalIntegrity,ciCheckSignals,undocumentedExport,staleBranch,commitHygiene
# pendingReviewRequests,testRatio,migrationSafety,looseRange,terminology,todoMarker,magicNumber
# conflictMarker,debugLeftover,commitLint
# deep: dependency,lockfileDrift,secret,license,installScript,heavyDependency,hardcodedUrl
# actionPin,eol,redos,provenance,codeowners,secretLog,assetWeight,typosquat,commitSignature
# iacMisconfig,nativeBuild,history,docCommentDrift,duplication,churnHotspot,blameLink
# approvalIntegrity,ciCheckSignals,undocumentedExport,staleBranch,commitHygiene
#
# Profile defaults:
# fast: dependency,dependencyDiff,lockfileDrift,secret,license,installScript,heavyDependency
# hardcodedUrl,actionPin,eol,redos,provenance,secretLog,typosquat,iacMisconfig,nativeBuild
# testRatio,migrationSafety,looseRange,terminology,todoMarker,magicNumber,conflictMarker
# debugLeftover
# balanced (default): dependency,dependencyDiff,lockfileDrift,secret,license,installScript
# heavyDependency,hardcodedUrl,actionPin,eol,redos,provenance,codeowners,secretLog,assetWeight
# typosquat,commitSignature,iacMisconfig,nativeBuild,history,docCommentDrift,duplication
# churnHotspot,blameLink,approvalIntegrity,ciCheckSignals,undocumentedExport,staleBranch
# commitHygiene,pendingReviewRequests,testRatio,migrationSafety,looseRange,terminology
# todoMarker,magicNumber,conflictMarker,debugLeftover,commitLint
# deep: dependency,dependencyDiff,lockfileDrift,secret,license,installScript,heavyDependency
# hardcodedUrl,actionPin,eol,redos,provenance,codeowners,secretLog,assetWeight,typosquat
# commitSignature,iacMisconfig,nativeBuild,history,docCommentDrift,duplication,churnHotspot
# blameLink,approvalIntegrity,ciCheckSignals,undocumentedExport,staleBranch,commitHygiene
# pendingReviewRequests,testRatio,migrationSafety,looseRange,terminology,todoMarker,magicNumber
# conflictMarker,debugLeftover,commitLint
# END GENERATED REES ANALYZERS
Expand Down
23 changes: 23 additions & 0 deletions apps/gittensory-ui/src/lib/rees-analyzers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,29 @@ export const REES_ANALYZERS = [
notes: "Manifest-only by design; use lockfileDrift for transitive lockfile changes.",
},
},
{
name: "dependencyDiff",
title: "Dependency inventory changes",
category: "supply-chain",
cost: "local",
defaultEnabled: true,
profiles: ["fast", "balanced", "deep"],
requires: ["files"],
limits: {
maxFindings: 25,
maxManifestFiles: 20,
maxPatchLinesPerFile: 500,
},
docs: {
summary:
"Summarizes direct dependency add/remove/version-change deltas in changed manifest patches — informational, not a CVE scan.",
looksAt: "Added/removed lines in package.json, requirements.txt, and go.mod diffs.",
reports: "Ecosystem, package name, direction (add/remove/change), and from/to versions.",
network: "Pure local analyzer. No external network call.",
notes:
"Distinct from the dependency CVE analyzer: this is the neutral inventory delta only. Reuses the same manifest line parsers.",
},
},
{
name: "lockfileDrift",
title: "Lockfile drift",
Expand Down
27 changes: 27 additions & 0 deletions review-enrichment/analyzer-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,33 @@
"notes": "Manifest-only by design; use lockfileDrift for transitive lockfile changes."
}
},
{
"name": "dependencyDiff",
"title": "Dependency inventory changes",
"category": "supply-chain",
"cost": "local",
"defaultEnabled": true,
"profiles": [
"fast",
"balanced",
"deep"
],
"requires": [
"files"
],
"limits": {
"maxFindings": 25,
"maxManifestFiles": 20,
"maxPatchLinesPerFile": 500
},
"docs": {
"summary": "Summarizes direct dependency add/remove/version-change deltas in changed manifest patches — informational, not a CVE scan.",
"looksAt": "Added/removed lines in package.json, requirements.txt, and go.mod diffs.",
"reports": "Ecosystem, package name, direction (add/remove/change), and from/to versions.",
"network": "Pure local analyzer. No external network call.",
"notes": "Distinct from the dependency CVE analyzer: this is the neutral inventory delta only. Reuses the same manifest line parsers."
}
},
{
"name": "lockfileDrift",
"title": "Lockfile drift",
Expand Down
28 changes: 28 additions & 0 deletions review-enrichment/src/analyzers/dependency-diff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Dependency-diff inventory analyzer (#2020). Emits a neutral summary of direct dependency manifest
// changes — added, removed, or version-changed packages across package.json, requirements.txt, and go.mod.
// Distinct from the CVE-scanning dependency analyzer: no registry calls, pure compute over manifest patches.
import type { DependencyDiffFinding, EnrichRequest } from "../types.js";
import {
extractDependencyInventoryChanges,
type ScanLimits,
} from "./dependency-scan.js";

const MAX_FINDINGS = 25;
const LIMITS: ScanLimits = {
maxManifestFiles: 20,
maxPatchLinesPerFile: 500,
};

/** Scan changed manifest patches for direct dependency inventory deltas. Pure. */
export function scanDependencyDiff(req: EnrichRequest): DependencyDiffFinding[] {
return extractDependencyInventoryChanges(req.files ?? [], LIMITS, MAX_FINDINGS);
}

/** Analyzer entrypoint: summarize direct dependency add/remove/change deltas from manifest patches. */
export async function scanDependencyDiffInventory(
req: EnrichRequest,
signal?: AbortSignal,
): Promise<DependencyDiffFinding[]> {
if (signal?.aborted) throw new Error("analyzer_aborted");
return scanDependencyDiff(req);
}
69 changes: 64 additions & 5 deletions review-enrichment/src/analyzers/dependency-scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,12 @@ const ECOSYSTEM: Record<string, string> = {
"go.mod": "Go",
};

/** Extract added/changed (not removed) dependency versions from the changed manifests in the diff. Pure. */
export function extractDependencyChanges(
/** Collect direct-dependency add/remove versions from changed manifest patches. Pure. */
function collectManifestDependencyEntries(
files: NonNullable<EnrichRequest["files"]>,
limits: ScanLimits = {},
): DepChange[] {
keyByFile = false,
): Map<string, { ecosystem: string; package: string; added?: string; removed?: string }> {
const byKey = new Map<
string,
{ ecosystem: string; package: string; added?: string; removed?: string }
Expand All @@ -112,15 +113,25 @@ export function extractDependencyChanges(
continue;
const parsed = parseLine(manifest, line.slice(1).trim());
if (!parsed) continue;
const key = ecosystem + "::" + parsed.name;
const key = keyByFile
? `${file.path}::${ecosystem}::${parsed.name}`
: `${ecosystem}::${parsed.name}`;
const entry = byKey.get(key) ?? { ecosystem, package: parsed.name };
if (sign === "+") entry.added = parsed.version;
else entry.removed = parsed.version;
byKey.set(key, entry);
}
}
return byKey;
}

/** Extract added/changed (not removed) dependency versions from the changed manifests in the diff. Pure. */
export function extractDependencyChanges(
files: NonNullable<EnrichRequest["files"]>,
limits: ScanLimits = {},
): DepChange[] {
const changes: DepChange[] = [];
for (const entry of byKey.values()) {
for (const entry of collectManifestDependencyEntries(files, limits).values()) {
// Only scan a version that's present after the change, and only when it actually changed.
if (!entry.added || entry.added === entry.removed) continue;
changes.push({
Expand All @@ -133,6 +144,54 @@ export function extractDependencyChanges(
return changes;
}

export type DependencyInventoryDirection = "add" | "remove" | "change";

export interface DependencyInventoryChange {
ecosystem: string;
package: string;
from: string | null;
to: string | null;
direction: DependencyInventoryDirection;
}

/** Neutral direct-dependency inventory delta from manifest patches — add, remove, and version change. Pure. */
export function extractDependencyInventoryChanges(
files: NonNullable<EnrichRequest["files"]>,
limits: ScanLimits = {},
maxFindings = 25,
): DependencyInventoryChange[] {
const findings: DependencyInventoryChange[] = [];
for (const entry of collectManifestDependencyEntries(files, limits, true).values()) {
if (findings.length >= maxFindings) break;
if (entry.added && entry.removed && entry.added !== entry.removed) {
findings.push({
ecosystem: entry.ecosystem,
package: entry.package,
from: entry.removed,
to: entry.added,
direction: "change",
});
} else if (entry.added && !entry.removed) {
findings.push({
ecosystem: entry.ecosystem,
package: entry.package,
from: null,
to: entry.added,
direction: "add",
});
} else if (entry.removed && !entry.added) {
findings.push({
ecosystem: entry.ecosystem,
package: entry.package,
from: entry.removed,
to: null,
direction: "remove",
});
}
}
return findings;
}

interface OsvVuln {
id: string;
summary?: string;
Expand Down
36 changes: 36 additions & 0 deletions review-enrichment/src/analyzers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { scanCodeowners } from "./codeowners.js";
import { scanCommitHygiene } from "./commit-hygiene.js";
import { scanCommitSignature } from "./commit-signature.js";
import { dependencyAnalyzer } from "./dependency/descriptor.js";
import { scanDependencyDiffInventory } from "./dependency-diff.js";
import { scanDocCommentDrift } from "./doc-comment-drift.js";
import { scanDuplication } from "./duplication-scan.js";
import { scanEol } from "./eol-check.js";
Expand Down Expand Up @@ -52,6 +53,41 @@ function descriptor<Name extends AnalyzerName>(

export const ANALYZER_DESCRIPTORS = [
dependencyAnalyzer,
descriptor({
name: "dependencyDiff",
title: "Dependency inventory changes",
category: "supply-chain",
cost: "local",
defaultEnabled: true,
requires: ["files"],
limits: { maxFindings: 25, maxManifestFiles: 20, maxPatchLinesPerFile: 500 },
docs: {
summary:
"Summarizes direct dependency add/remove/version-change deltas in changed manifest patches — informational, not a CVE scan.",
looksAt: "Added/removed lines in package.json, requirements.txt, and go.mod diffs.",
reports: "Ecosystem, package name, direction (add/remove/change), and from/to versions.",
network: "Pure local analyzer. No external network call.",
notes:
"Distinct from the dependency CVE analyzer: this is the neutral inventory delta only. Reuses the same manifest line parsers.",
},
render: (findings, helpers) => {
if (!findings.length) return [];
const lines = ["### Dependency inventory changes (direct manifest delta)"];
for (const item of findings) {
const detail =
item.direction === "add"
? `added @ ${helpers.safeCodeSpan(item.to ?? "?")}`
: item.direction === "remove"
? `removed (was ${helpers.safeCodeSpan(item.from ?? "?")})`
: `${helpers.safeCodeSpan(item.from ?? "?")} → ${helpers.safeCodeSpan(item.to ?? "?")}`;
lines.push(
`- ${helpers.safeCodeSpan(item.package)} (${helpers.safeCodeSpan(item.ecosystem)}) — ${helpers.safeCodeSpan(item.direction)}: ${detail}`,
);
}
return lines;
},
run: (req, { signal }) => scanDependencyDiffInventory(req, signal),
}),
descriptor({
name: "lockfileDrift",
title: "Lockfile drift",
Expand Down
1 change: 1 addition & 0 deletions review-enrichment/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function renderBrief(
const lines: string[] = [];

lines.push(...renderDescriptorSection("dependency", findings.dependency));
lines.push(...renderDescriptorSection("dependencyDiff", findings.dependencyDiff));

const lockfileDrift = findings.lockfileDrift ?? [];
if (lockfileDrift.length) {
Expand Down
11 changes: 11 additions & 0 deletions review-enrichment/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ export interface DependencyFinding {
cves: Cve[];
}

/** Neutral direct-dependency inventory delta from manifest patches (#2020, part of #1499).
* Reports add/remove/version-change only — no CVE or registry data. */
export interface DependencyDiffFinding {
ecosystem: string;
package: string;
from: string | null;
to: string | null;
direction: "add" | "remove" | "change";
}

/** A vulnerable lockfile-only dependency resolution. The package was not changed in a top-level manifest diff,
* so it is treated as transitive lockfile drift and reported with the lockfile location that introduced it. */
export interface LockfileDriftFinding {
Expand Down Expand Up @@ -493,6 +503,7 @@ export interface CommitLintFinding {
/** Structured analyzer output. Each analyzer fills its own key; more land as analyzers ship (#1477/#1478). */
export interface BriefFindings {
dependency?: DependencyFinding[];
dependencyDiff?: DependencyDiffFinding[];
lockfileDrift?: LockfileDriftFinding[];
secret?: SecretFinding[];
license?: LicenseFinding[];
Expand Down
1 change: 1 addition & 0 deletions review-enrichment/test/analyzer-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { renderBrief } from "../dist/render.js";

const EXPECTED_ANALYZERS = [
"dependency",
"dependencyDiff",
"lockfileDrift",
"secret",
"license",
Expand Down
Loading
Loading