Skip to content
Closed
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
78 changes: 78 additions & 0 deletions scripts/quality/check-direct-dependency-evidence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import {
checkDirectDependencyEvidence,
extractDirectDependencyAdditions,
} from './check-direct-dependency-evidence';

const fixtureRoot = join(import.meta.dirname, 'fixtures/supply-chain');
const readFixture = (name: string) => readFileSync(join(fixtureRoot, name), 'utf8');

describe('direct dependency evidence checker', () => {
it('requires authoritative evidence and necessity for production npm additions', () => {
const result = checkDirectDependencyEvidence(readFixture('direct-dependency-add.diff'), {
npm: {},
});

expect(result.ok).toBe(false);
expect(result.errors).toContain('Missing direct dependency evidence for npm:left-pad');
});

it('accepts registry/homepage evidence with a one-line necessity', () => {
const result = checkDirectDependencyEvidence(readFixture('direct-dependency-add.diff'), {
npm: {
'left-pad': {
registryUrl: 'https://www.npmjs.com/package/left-pad',
necessity: 'Pads deterministic fixture values.',
},
},
});

expect(result.ok).toBe(true);
});

it('flags malformed evidence without printing package contents', () => {
const result = checkDirectDependencyEvidence(readFixture('go-dependency-add.diff'), {
go: {
'golang.org/x/crypto': {
registryUrl: 'http://pkg.go.dev/golang.org/x/crypto',
necessity: 'crypto',
},
},
});

expect(result.ok).toBe(false);
expect(result.errors).toEqual([
'go:golang.org/x/crypto registryUrl must be an https URL',
'go:golang.org/x/crypto needs a one-line necessity with at least three words',
]);
});

it('does not require evidence for removals, version updates, dev dependencies, or workspace/internal dependencies', () => {
const diffs = [
'dependency-update-remove.diff',
'direct-dependency-dev.diff',
'direct-dependency-workspace.diff',
].map(readFixture);

for (const diff of diffs) {
const result = checkDirectDependencyEvidence(diff, {});
expect(result.ok).toBe(true);
}
});

it('extracts Go direct additions from module manifests only', () => {
const additions = extractDirectDependencyAdditions(readFixture('go-dependency-add.diff'));

expect(additions).toEqual([
{
ecosystem: 'go',
manifestPath: 'packages/vm-agent/go.mod',
name: 'golang.org/x/crypto',
production: true,
internal: false,
},
]);
});
});
240 changes: 240 additions & 0 deletions scripts/quality/check-direct-dependency-evidence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { basename, dirname, join, posix } from 'node:path';
import { fileURLToPath } from 'node:url';

export type DependencyEcosystem = 'npm' | 'go';

export interface DependencyAddition {
ecosystem: DependencyEcosystem;
manifestPath: string;
name: string;
production: boolean;
internal: boolean;
}

export interface DependencyEvidence {
registryUrl?: string;
homepageUrl?: string;
necessity?: string;
}

export interface EvidenceFile {
npm?: Record<string, DependencyEvidence>;
go?: Record<string, DependencyEvidence>;
}

export interface CheckResult {
ok: boolean;
additions: DependencyAddition[];
errors: string[];
}

const repoRoot = posix.normalize(join(dirname(fileURLToPath(import.meta.url)), '../..'));
const evidencePath = join(repoRoot, 'scripts/quality/direct-dependency-evidence.json');
const urlPattern = /^https:\/\/[^\s"<>]+$/;
const goRequirePattern =
/^\+\s*(?:require\s+)?([A-Za-z0-9_.~/-]+\.[A-Za-z0-9_.~/-]+)\s+v[^\s]+(?:\s*\/\/.*)?$/;

Check warning on line 37 in scripts/quality/check-direct-dependency-evidence.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Simplify this regular expression to reduce its runtime, as it has super-linear performance due to backtracking.

See more on https://sonarcloud.io/project/issues?id=raphaeltm_simple-agent-manager&issues=AZ_niNm6MZUky4BdUzCD&open=AZ_niNm6MZUky4BdUzCD&pullRequest=1781

function readJson(path: string): unknown {
return JSON.parse(readFileSync(path, 'utf8')) as unknown;
}

function normalizeRepoPath(path: string): string {
return posix.normalize(path.replaceAll('\\', '/'));
}

function changedFileFromDiffLine(line: string): string | undefined {
const match = /^\+\+\+ b\/(.+)$/.exec(line);
return match?.[1] ? normalizeRepoPath(match[1]) : undefined;
}

function packageDependencySection(line: string): 'dependencies' | 'devDependencies' | undefined {
const match = /^\s*[+-]?\s*"([^"]+)":\s*\{?\s*$/.exec(line);

Check warning on line 53 in scripts/quality/check-direct-dependency-evidence.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Simplify this regular expression to reduce its runtime, as it has super-linear performance due to backtracking.

See more on https://sonarcloud.io/project/issues?id=raphaeltm_simple-agent-manager&issues=AZ_niNm6MZUky4BdUzCE&open=AZ_niNm6MZUky4BdUzCE&pullRequest=1781
const key = match?.[1];
if (key === 'dependencies' || key === 'devDependencies') return key;
return undefined;
}

function npmDependencyFromAddedLine(line: string): { name: string; version: string } | undefined {
const match = /^\+\s*"([^"]+)":\s*"([^"]+)"\s*,?\s*$/.exec(line);

Check warning on line 60 in scripts/quality/check-direct-dependency-evidence.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Simplify this regular expression to reduce its runtime, as it has super-linear performance due to backtracking.

See more on https://sonarcloud.io/project/issues?id=raphaeltm_simple-agent-manager&issues=AZ_niNm6MZUky4BdUzCF&open=AZ_niNm6MZUky4BdUzCF&pullRequest=1781
if (!match?.[1] || !match[2]) return undefined;
return { name: match[1], version: match[2] };
}

function npmDependencyFromRemovedLine(line: string): { name: string; version: string } | undefined {
const match = /^-\s*"([^"]+)":\s*"([^"]+)"\s*,?\s*$/.exec(line);

Check warning on line 66 in scripts/quality/check-direct-dependency-evidence.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Simplify this regular expression to reduce its runtime, as it has super-linear performance due to backtracking.

See more on https://sonarcloud.io/project/issues?id=raphaeltm_simple-agent-manager&issues=AZ_niNm6MZUky4BdUzCG&open=AZ_niNm6MZUky4BdUzCG&pullRequest=1781
if (!match?.[1] || !match[2]) return undefined;
return { name: match[1], version: match[2] };
}

function nearestWorkspacePackageName(manifestPath: string): string | undefined {
try {
const manifest = readJson(join(repoRoot, manifestPath));
if (
typeof manifest === 'object' &&
manifest !== null &&
'name' in manifest &&
typeof manifest.name === 'string'
) {
return manifest.name;
}
} catch {
return undefined;
}
return undefined;
}

function isInternalNpmDependency(name: string, version: string, manifestPath: string): boolean {
if (version.startsWith('workspace:')) return true;
if (name.startsWith('@simple-agent-manager/')) return true;
return nearestWorkspacePackageName(manifestPath) === name;
}

function isInternalGoDependency(name: string): boolean {
return name.startsWith('github.com/raphaeltm/simple-agent-manager/');
}

export function extractDirectDependencyAdditions(diff: string): DependencyAddition[] {

Check failure on line 98 in scripts/quality/check-direct-dependency-evidence.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 44 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=raphaeltm_simple-agent-manager&issues=AZ_niNm6MZUky4BdUzCH&open=AZ_niNm6MZUky4BdUzCH&pullRequest=1781
const additions: DependencyAddition[] = [];
const removedDependencies = new Set<string>();
let currentFile: string | undefined;
let npmSection: 'dependencies' | 'devDependencies' | undefined;
let inGoRequireBlock = false;

for (const line of diff.split('\n')) {
const nextFile = changedFileFromDiffLine(line);
if (nextFile) {
currentFile = nextFile;
npmSection = undefined;
inGoRequireBlock = false;
continue;
}

if (!currentFile) continue;

if (basename(currentFile) === 'package.json') {
const section = packageDependencySection(line);
if (section) npmSection = section;
if (/^\s*[+-]?\s*}\s*,?\s*$/.test(line)) npmSection = undefined;

Check warning on line 119 in scripts/quality/check-direct-dependency-evidence.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Simplify this regular expression to reduce its runtime, as it has super-linear performance due to backtracking.

See more on https://sonarcloud.io/project/issues?id=raphaeltm_simple-agent-manager&issues=AZ_niNm6MZUky4BdUzCI&open=AZ_niNm6MZUky4BdUzCI&pullRequest=1781

const removedDependency = npmDependencyFromRemovedLine(line);
if (removedDependency && npmSection) {
removedDependencies.add(`${currentFile}\0${npmSection}\0${removedDependency.name}`);
}

const dependency = npmDependencyFromAddedLine(line);
if (dependency && npmSection) {
if (removedDependencies.has(`${currentFile}\0${npmSection}\0${dependency.name}`)) continue;
additions.push({
ecosystem: 'npm',
manifestPath: currentFile,
name: dependency.name,
production: npmSection === 'dependencies',
internal: isInternalNpmDependency(dependency.name, dependency.version, currentFile),
});
}
continue;
}

if (basename(currentFile) === 'go.mod') {
if (/^[ +]?require\s*\(\s*$/.test(line)) {
inGoRequireBlock = true;
continue;
}
if (inGoRequireBlock && /^[ +]?\)\s*$/.test(line)) {
inGoRequireBlock = false;
continue;
}
if (!line.startsWith('+') || line.startsWith('+++')) continue;
const match = goRequirePattern.exec(line);
if (match?.[1]) {
const removedSameModulePattern = new RegExp(
`^-\\s*(?:require\\s+)?${match[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s+v\\S+`

Check warning on line 153 in scripts/quality/check-direct-dependency-evidence.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`String.raw` should be used to avoid escaping `\`.

See more on https://sonarcloud.io/project/issues?id=raphaeltm_simple-agent-manager&issues=AZ_niNm6MZUky4BdUzCK&open=AZ_niNm6MZUky4BdUzCK&pullRequest=1781

Check warning on line 153 in scripts/quality/check-direct-dependency-evidence.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`String.raw` should be used to avoid escaping `\`.

See more on https://sonarcloud.io/project/issues?id=raphaeltm_simple-agent-manager&issues=AZ_niNm6MZUky4BdUzCJ&open=AZ_niNm6MZUky4BdUzCJ&pullRequest=1781
);
if (diff.split('\n').some((diffLine) => removedSameModulePattern.test(diffLine))) continue;
additions.push({
ecosystem: 'go',
manifestPath: currentFile,
name: match[1],
production: true,
internal: isInternalGoDependency(match[1]),
});
}
}
}

return additions;
}

function validateEvidenceEntry(
addition: DependencyAddition,
evidence: DependencyEvidence | undefined
): string[] {
const prefix = `${addition.ecosystem}:${addition.name}`;
const errors: string[] = [];
if (!evidence) return [`Missing direct dependency evidence for ${prefix}`];

if (!evidence.registryUrl && !evidence.homepageUrl) {
errors.push(`${prefix} needs registryUrl or homepageUrl`);
}
if (evidence.registryUrl && !urlPattern.test(evidence.registryUrl)) {
errors.push(`${prefix} registryUrl must be an https URL`);
}
if (evidence.homepageUrl && !urlPattern.test(evidence.homepageUrl)) {
errors.push(`${prefix} homepageUrl must be an https URL`);
}
if (!evidence.necessity || evidence.necessity.trim().split(/\s+/).length < 3) {
errors.push(`${prefix} needs a one-line necessity with at least three words`);
}
if (evidence.necessity?.includes('\n')) {
errors.push(`${prefix} necessity must be one line`);
}
return errors;
}

export function checkDirectDependencyEvidence(diff: string, evidence: EvidenceFile): CheckResult {
const additions = extractDirectDependencyAdditions(diff);
const relevantAdditions = additions.filter(
(addition) => addition.production && !addition.internal
);
const errors = relevantAdditions.flatMap((addition) =>
validateEvidenceEntry(addition, evidence[addition.ecosystem]?.[addition.name])
);
return { ok: errors.length === 0, additions, errors };
}

export function loadEvidence(path = evidencePath): EvidenceFile {
if (!existsSync(path)) return {};
const parsed = readJson(path);
if (typeof parsed !== 'object' || parsed === null) return {};
return parsed as EvidenceFile;
}

function diffAgainstBase(): string {
const base = process.env.GITHUB_BASE_REF
? `origin/${process.env.GITHUB_BASE_REF}`
: 'origin/main';
return execFileSync(
'git',

Check warning on line 219 in scripts/quality/check-direct-dependency-evidence.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make sure the "PATH" variable only contains fixed, unwriteable directories.

See more on https://sonarcloud.io/project/issues?id=raphaeltm_simple-agent-manager&issues=AZ_niNm6MZUky4BdUzCL&open=AZ_niNm6MZUky4BdUzCL&pullRequest=1781
['diff', '--unified=0', `${base}...HEAD`, '--', '**/package.json', '**/go.mod'],
{
cwd: repoRoot,
encoding: 'utf8',
}
);
}

if (import.meta.url === `file://${process.argv[1]}`) {
const result = checkDirectDependencyEvidence(diffAgainstBase(), loadEvidence());
if (!result.ok) {
console.error(
[
'Direct dependency evidence check failed:',
...result.errors.map((error) => `- ${error}`),
].join('\n')
);
process.exit(1);
}
console.log('Direct dependency evidence check passed.');
}
28 changes: 28 additions & 0 deletions scripts/quality/check-go-vulnerability-diff.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, it, vi } from 'vitest';
import { runGoVulnerabilityDiffPolicy, shouldRunGovulncheck } from './check-go-vulnerability-diff';

describe('govulncheck diff policy', () => {
it('is applicable only when Go module files changed', () => {
expect(shouldRunGovulncheck(['apps/api/src/index.ts', 'README.md'])).toBe(false);
expect(shouldRunGovulncheck(['packages/vm-agent/go.mod'])).toBe(true);
expect(shouldRunGovulncheck(['packages/vm-agent/go.sum'])).toBe(true);
expect(shouldRunGovulncheck(['docs/go.mod-notes.md'])).toBe(false);
});

it('does not invoke govulncheck for unrelated diffs', () => {
const runner = vi.fn();
const result = runGoVulnerabilityDiffPolicy(['scripts/quality/check.test.ts'], runner);

expect(result).toMatchObject({ applicable: false, ok: true });
expect(runner).not.toHaveBeenCalled();
});

it('fails closed when govulncheck reports vulnerabilities or execution failure', () => {
const runner = vi.fn(() => ({ status: 1, stdout: 'vulnerability found', stderr: '' }));
const result = runGoVulnerabilityDiffPolicy(['packages/vm-agent/go.mod'], runner);

expect(result.ok).toBe(false);
expect(result.applicable).toBe(true);
expect(runner).toHaveBeenCalledWith('govulncheck', ['./...'], expect.any(Object));
});
});
Loading