forked from Talenttrust/Talenttrust-Backend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdependency-scan-service.ts
More file actions
123 lines (99 loc) · 3.33 KB
/
Copy pathdependency-scan-service.ts
File metadata and controls
123 lines (99 loc) · 3.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import { spawn } from 'node:child_process';
import { buildRemediationPlan, DEFAULT_DEPENDENCY_POLICY, evaluateDependencyPolicy } from './dependency-policy';
import { parseNpmAuditReport } from './npm-audit-parser';
import { DependencyPolicy, DependencyScanError, DependencyScanResult, NpmAuditReport } from './dependency-types';
export interface CommandResult {
stdout: string;
stderr: string;
exitCode: number;
}
export type CommandRunner = (command: string, args: string[]) => Promise<CommandResult>;
export interface DependencyScanProvider {
getLatestScan(forceRefresh?: boolean): Promise<DependencyScanResult>;
}
export const defaultCommandRunner: CommandRunner = (command, args) =>
new Promise((resolve) => {
const child = spawn(command, args, {
stdio: ['ignore', 'pipe', 'pipe'],
env: process.env,
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => {
stdout += chunk.toString();
});
child.stderr.on('data', (chunk) => {
stderr += chunk.toString();
});
child.on('error', (error) => {
resolve({
stdout,
stderr: `${stderr}\n${error.message}`.trim(),
exitCode: 1,
});
});
child.on('close', (code) => {
resolve({
stdout,
stderr,
exitCode: code ?? 1,
});
});
});
/**
* @notice Executes dependency scanning with cache and policy evaluation support.
*/
export class DependencyScanService implements DependencyScanProvider {
private latestResult?: DependencyScanResult;
private latestResultExpiresAt = 0;
public constructor(
private readonly runner: CommandRunner = defaultCommandRunner,
private readonly policy: DependencyPolicy = DEFAULT_DEPENDENCY_POLICY,
private readonly ttlMs = 5 * 60 * 1000,
private readonly now = () => Date.now(),
) {}
public async getLatestScan(forceRefresh = false): Promise<DependencyScanResult> {
if (!forceRefresh && this.latestResult && this.now() < this.latestResultExpiresAt) {
return this.latestResult;
}
const result = await this.performScan();
this.latestResult = result;
this.latestResultExpiresAt = this.now() + this.ttlMs;
return result;
}
private async performScan(): Promise<DependencyScanResult> {
const scannedAt = new Date(this.now()).toISOString();
const args = ['audit', '--json'];
if (!this.policy.includeDevDependencies) {
args.push('--omit=dev');
}
const auditResult = await this.runner('npm', args);
if (!auditResult.stdout.trim()) {
return this.createError(scannedAt, `npm audit returned no parsable output. ${auditResult.stderr}`.trim());
}
let parsed: NpmAuditReport;
try {
parsed = JSON.parse(auditResult.stdout) as NpmAuditReport;
} catch (error) {
return this.createError(scannedAt, `Invalid npm audit JSON output: ${(error as Error).message}`);
}
const summary = parseNpmAuditReport(parsed);
const evaluation = evaluateDependencyPolicy(summary, this.policy);
return {
status: 'ok',
scannedAt,
policy: this.policy,
summary,
evaluation,
remediation: buildRemediationPlan(summary),
};
}
private createError(scannedAt: string, message: string): DependencyScanError {
return {
status: 'error',
scannedAt,
policy: this.policy,
message,
};
}
}