dep-drift-sec 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # dep-drift-sec
2
+
3
+ **Production-grade security and stability guardrails for your Node.js dependencies.**
4
+
5
+ `dep-drift-sec` is an open-source CLI tool designed to prevent production breakage and identify supply-chain risks. It bridges the gap between basic vulnerability scanners and manual audits by focusing on **dependency drift**, **transitive relationships**, and **maintenance heuristics**.
6
+
7
+ > [!NOTE]
8
+ > The CLI is open-source and works entirely offline/locally without any mandatory backend service.
9
+
10
+ ---
11
+
12
+ ## Key Features
13
+
14
+ - **Drift Protection**: Detects flexible version ranges (`^`, `~`) in `package.json` and transitive version conflicts that cause "works on my machine" bugs.
15
+ - **Transitive Visibility**: Explicitly surfaces the full dependency chain for every risky package found.
16
+ - **Maintenance Heuristics**:
17
+ - **Unmaintained**: Identifies packages not updated in the last 18 months.
18
+ - **Deprecated**: Alerts on packages officially marked as deprecated by maintainers.
19
+ - **Single-Maintainer**: Highlights potential single-point-of-failure risks in your supply chain.
20
+ - **CI/CD Ready**: Machine-readable JSON output and standardized exit codes for easy pipeline integration.
21
+
22
+ ---
23
+
24
+ ## Getting Started
25
+
26
+ ### Installation (Development)
27
+ ```bash
28
+ npm install
29
+ npm run build
30
+ ```
31
+
32
+ ### Usage
33
+
34
+ #### Using `npx` (Recommended for CI/Production)
35
+ ```bash
36
+ # Run a check on the current directory
37
+ npx dep-drift-sec check
38
+
39
+ # Run with JSON output and SaaS-ready flag
40
+ npx dep-drift-sec check --json --upload
41
+
42
+ # Check a specific project path
43
+ npx dep-drift-sec check --path ./my-project
44
+ ```
45
+
46
+ #### Using Local Scripts
47
+ ```bash
48
+ # Quick scan (requires local build)
49
+ npm run scan
50
+
51
+ # Direct execution
52
+ node dist/cli/index.js check
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Scan Results Contract (v1.0)
58
+
59
+ When run with `--json`, the CLI produces a versioned, stable JSON structure.
60
+
61
+ ### Example Output
62
+ ```json
63
+ {
64
+ "meta": {
65
+ "schemaVersion": "1.0",
66
+ "scanId": "550e8400-e29b-41d4-a716-446655440000",
67
+ "projectName": "my-project",
68
+ "projectId": "a3f5b2c...",
69
+ "generatedAt": "2026-01-17T20:55:00Z"
70
+ },
71
+ "summary": {
72
+ "driftCount": 0,
73
+ "securityCount": 1,
74
+ "riskLevel": "medium",
75
+ "riskReason": "1 dependency has security or drift issues, increasing breakage and security risk.",
76
+ "recommendedAction": "warn",
77
+ "recommendedExitCode": 2
78
+ },
79
+ "drift": [],
80
+ "security": [
81
+ {
82
+ "dependencyName": "example-pkg",
83
+ "transitive": true,
84
+ "introducedBy": ["direct-parent"],
85
+ "description": "An example package",
86
+ "issues": [
87
+ {
88
+ "type": "unmaintained",
89
+ "reason": "Last update was 70 months ago.",
90
+ "riskLevel": "medium",
91
+ "details": { "lastUpdate": "2020-03-04" }
92
+ }
93
+ ],
94
+ "overallRisk": "medium"
95
+ }
96
+ ]
97
+ }
98
+ ```
99
+
100
+ ---
101
+
102
+ ## CI/CD Integration (Exit Codes)
103
+
104
+ | Code | Meaning | Outcome |
105
+ | :--- | :--- | :--- |
106
+ | `0` | **OK** | No drift or security issues found. |
107
+ | `1` | **Drift Detected** | Version ranges found in `package.json`. |
108
+ | `2` | **Security Issue** | Heuristics triggered (unmaintained, etc). |
109
+ | `3` | **Mixed Issues** | Both drift and security issues present. |
110
+ | `4` | **Internal Error** | Missing files or runtime failure. |
111
+
112
+ ---
113
+
114
+ ## License
115
+
116
+ MIT - See [LICENSE](./LICENSE) for details.
@@ -0,0 +1,17 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ export async function loadProjectData(projectPath) {
4
+ const packageJsonPath = path.join(projectPath, 'package.json');
5
+ const packageLockPath = path.join(projectPath, 'package-lock.json');
6
+ try {
7
+ const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8');
8
+ const packageLockContent = await fs.readFile(packageLockPath, 'utf8');
9
+ return {
10
+ packageJson: JSON.parse(packageJsonContent),
11
+ packageLock: JSON.parse(packageLockContent),
12
+ };
13
+ }
14
+ catch (error) {
15
+ throw new Error(`Failed to load project data from ${projectPath}: ${error.message}`);
16
+ }
17
+ }
@@ -0,0 +1,30 @@
1
+ import axios from 'axios';
2
+ const REGISTRY_URL = 'https://registry.npmjs.org';
3
+ export async function fetchNpmMetadata(packageName) {
4
+ try {
5
+ const response = await axios.get(`${REGISTRY_URL}/${packageName}`, {
6
+ timeout: 5000,
7
+ });
8
+ return response.data;
9
+ }
10
+ catch (error) {
11
+ // If not found or error, return null (might be private or deleted)
12
+ return null;
13
+ }
14
+ }
15
+ export async function fetchAllMetadata(packageNames) {
16
+ const metadataMap = {};
17
+ // For MVP, we fetch sequentially or in small chunks to avoid registry rate limits
18
+ // but since we might have many transitive deps, we'll do them in parallel with a limit if needed.
19
+ // Here we'll just do a simple Promise.all for now.
20
+ const results = await Promise.all(packageNames.map(async (name) => {
21
+ const meta = await fetchNpmMetadata(name);
22
+ return { name, meta };
23
+ }));
24
+ for (const { name, meta } of results) {
25
+ if (meta) {
26
+ metadataMap[name] = meta;
27
+ }
28
+ }
29
+ return metadataMap;
30
+ }
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+ import { loadProjectData } from '../adapters/fs-loader.js';
6
+ import { fetchAllMetadata } from '../adapters/npm-registry.js';
7
+ import { detectDrift } from '../core/drift.js';
8
+ import { analyzeSecurity } from '../core/security.js';
9
+ import { formatConsoleReport } from '../report/console-report.js';
10
+ import { formatJsonReport } from '../report/json-report.js';
11
+ import { buildDependencyGraph } from '../core/graph.js';
12
+ const program = new Command();
13
+ program
14
+ .name('dep-drift-sec')
15
+ .description('Detect dependency drift and basic security risks')
16
+ .version('1.0.0');
17
+ program
18
+ .command('check')
19
+ .description('Run drift and security checks on the current project')
20
+ .option('--json', 'Output report in JSON format')
21
+ .option('--path <path>', 'Path to the Node project', '.')
22
+ .option('--upload', 'Upload results to SaaS (reserved)')
23
+ .action(async (options) => {
24
+ try {
25
+ if (options.upload) {
26
+ console.warn('Warning: Upload is not enabled in the open-source CLI.');
27
+ }
28
+ const projectPath = path.resolve(process.cwd(), options.path);
29
+ const project = await loadProjectData(projectPath);
30
+ // Generate stable projectId from lockfile content
31
+ const lockfileContent = JSON.stringify(project.packageLock);
32
+ const projectId = crypto.createHash('sha256').update(lockfileContent).digest('hex');
33
+ const scanId = crypto.randomUUID();
34
+ const generatedAt = new Date().toISOString();
35
+ // 1. Extract unique deps for metadata fetching
36
+ const lockPackages = project.packageLock.packages || {};
37
+ const uniqueDeps = new Set();
38
+ for (const p of Object.keys(lockPackages)) {
39
+ if (p === '' || !p.startsWith('node_modules/'))
40
+ continue;
41
+ uniqueDeps.add(p.split('node_modules/').pop());
42
+ }
43
+ // 2. Fetch metadata
44
+ const metadataMap = await fetchAllMetadata(Array.from(uniqueDeps));
45
+ // 3. Build normalized graph
46
+ const graph = buildDependencyGraph(project, metadataMap, 'local');
47
+ // 4. Analyze
48
+ const driftGroups = detectDrift(graph);
49
+ const securityGroups = analyzeSecurity(graph);
50
+ // 5. Calculate Summary Totals
51
+ const totalDriftIssues = driftGroups.reduce((acc, g) => acc + g.issues.length, 0);
52
+ const uniqueSecurityDeps = securityGroups.length;
53
+ // 6. Determine overall risk factors
54
+ let overallRisk = 'low';
55
+ let riskReason = 'No significant risks detected. Your dependencies appear healthy.';
56
+ let recommendedAction = 'allow';
57
+ const affectedDeps = new Set([
58
+ ...securityGroups.map(g => g.dependencyName),
59
+ ...driftGroups.map(g => g.dependencyName)
60
+ ]);
61
+ const affectedCount = affectedDeps.size;
62
+ const transitiveCount = [
63
+ ...securityGroups.filter(g => g.transitive),
64
+ ...driftGroups.filter(g => g.transitive && !securityGroups.some(sg => sg.dependencyName === g.dependencyName))
65
+ ].length;
66
+ if (affectedCount > 0) {
67
+ const transText = transitiveCount > 0 ? ` (${transitiveCount} transitive)` : "";
68
+ riskReason = `${affectedCount} dependenc${affectedCount > 1 ? 'ies have' : 'y has'} security or drift issues${transText}, increasing breakage and security risk.`;
69
+ }
70
+ if (securityGroups.some(g => g.overallRisk === 'high')) {
71
+ overallRisk = 'high';
72
+ recommendedAction = 'block';
73
+ }
74
+ else if (securityGroups.some(g => g.overallRisk === 'medium') || driftGroups.length > 0) {
75
+ overallRisk = 'medium';
76
+ recommendedAction = 'warn';
77
+ }
78
+ else if (securityGroups.some(g => g.overallRisk === 'low')) {
79
+ overallRisk = 'low';
80
+ recommendedAction = 'allow';
81
+ }
82
+ // 7. Determine Exit Code (Preserved logic)
83
+ let exitCode = 0;
84
+ if (totalDriftIssues > 0 && securityGroups.length > 0)
85
+ exitCode = 3;
86
+ else if (totalDriftIssues > 0)
87
+ exitCode = 1;
88
+ else if (securityGroups.length > 0)
89
+ exitCode = 2;
90
+ const report = {
91
+ meta: {
92
+ schemaVersion: "1.0",
93
+ scanId,
94
+ projectName: project.packageJson.name || 'unknown',
95
+ projectId,
96
+ generatedAt,
97
+ },
98
+ summary: {
99
+ driftCount: totalDriftIssues,
100
+ securityCount: uniqueSecurityDeps,
101
+ riskLevel: overallRisk,
102
+ riskReason,
103
+ recommendedAction,
104
+ recommendedExitCode: exitCode,
105
+ },
106
+ drift: driftGroups,
107
+ security: securityGroups,
108
+ };
109
+ if (options.json) {
110
+ process.stdout.write(formatJsonReport(report) + '\n');
111
+ }
112
+ else {
113
+ process.stdout.write(formatConsoleReport(report) + '\n');
114
+ }
115
+ process.exitCode = exitCode;
116
+ }
117
+ catch (error) {
118
+ if (!options.json) {
119
+ console.error(`Error: ${error.message}`);
120
+ }
121
+ else {
122
+ // In JSON mode, we should still output valid JSON if possible,
123
+ // but for a fatal initialization error, we'll just exit with code 4.
124
+ // The requirements say "JSON output is always valid when --json is passed",
125
+ // so let's try to wrap the error.
126
+ process.stdout.write(JSON.stringify({ error: error.message, exitCode: 4 }) + '\n');
127
+ }
128
+ process.exitCode = 4;
129
+ }
130
+ });
131
+ program.parse(process.argv);
@@ -0,0 +1,52 @@
1
+ export function detectDrift(graph) {
2
+ const groups = [];
3
+ const groupMap = new Map();
4
+ const getGroup = (name) => {
5
+ if (!groupMap.has(name)) {
6
+ const node = graph.dependencies.find(d => d.name === name);
7
+ const group = {
8
+ dependencyName: name,
9
+ transitive: node?.transitive ?? false,
10
+ introducedBy: node?.introducedBy ?? [],
11
+ issues: []
12
+ };
13
+ groupMap.set(name, group);
14
+ groups.push(group);
15
+ }
16
+ return groupMap.get(name);
17
+ };
18
+ for (const node of graph.dependencies) {
19
+ // 1. Check for ^ or ~ on direct dependencies
20
+ if (!node.transitive) {
21
+ const range = node.version;
22
+ if (range.startsWith('^') || range.startsWith('~')) {
23
+ getGroup(node.name).issues.push({
24
+ dependencyName: node.name,
25
+ type: 'range-usage',
26
+ expected: range,
27
+ actual: range,
28
+ reason: `The dependency "${node.name}" uses a "${range[0]}" symbol (e.g., ^ or ~). This means npm might automatically install a newer version. To ensure everyone uses the exact same version, it is better to use a fixed version (e.g., "1.2.3" instead of "${range}").`
29
+ });
30
+ }
31
+ }
32
+ }
33
+ // 2. Detect multiple versions of the same dependency (Transitive Drift)
34
+ const versionMap = {};
35
+ for (const node of graph.dependencies) {
36
+ if (!versionMap[node.name])
37
+ versionMap[node.name] = new Set();
38
+ versionMap[node.name].add(node.resolvedVersion);
39
+ }
40
+ for (const [name, versions] of Object.entries(versionMap)) {
41
+ if (versions.size > 1) {
42
+ getGroup(name).issues.push({
43
+ dependencyName: name,
44
+ type: 'transitive-drift',
45
+ expected: Array.from(versions)[0],
46
+ actual: Array.from(versions).join(', '),
47
+ reason: `Multiple versions of "${name}" are present in your project (${Array.from(versions).join(', ')}). This often happens when different libraries require incompatible versions of the same dependency. This can increase project size and cause unpredictable bugs.`
48
+ });
49
+ }
50
+ }
51
+ return groups.filter(g => g.issues.length > 0);
52
+ }
@@ -0,0 +1,73 @@
1
+ export function buildDependencyGraph(project, metadataMap, environment = 'local') {
2
+ const { packageJson, packageLock } = project;
3
+ const dependencies = [];
4
+ const lockPackages = packageLock.packages || {};
5
+ const directDeps = {
6
+ ...(packageJson.dependencies || {}),
7
+ ...(packageJson.devDependencies || {})
8
+ };
9
+ // Build a map of dependency name -> list of parents that require it
10
+ // We use both the path (for nested npm v3 style) AND the dependencies field (for flat npm v7+ style)
11
+ const parentMap = {};
12
+ for (const [path, pkg] of Object.entries(lockPackages)) {
13
+ // 1. From Path Nesting (Fallback for minimized lockfiles or npm < 7)
14
+ const pathParts = path.split('node_modules/');
15
+ if (pathParts.length > 2) {
16
+ const depName = pathParts[pathParts.length - 1];
17
+ const parentName = pathParts[pathParts.length - 2].replace(/\/$/, '');
18
+ if (!parentMap[depName])
19
+ parentMap[depName] = new Set();
20
+ parentMap[depName].add(parentName);
21
+ }
22
+ // 2. From dependencies field (Standard npm behavior)
23
+ const currentPkgName = path === '' ? (packageJson.name || 'root') : path.split('node_modules/').pop();
24
+ const pkgDeps = {
25
+ ...(pkg.dependencies || {}),
26
+ ...(pkg.devDependencies || {}),
27
+ ...(pkg.optionalDependencies || {})
28
+ };
29
+ for (const depName of Object.keys(pkgDeps)) {
30
+ if (!parentMap[depName])
31
+ parentMap[depName] = new Set();
32
+ parentMap[depName].add(currentPkgName);
33
+ }
34
+ }
35
+ // Process nodes
36
+ const processedNodes = new Map();
37
+ for (const [path, pkg] of Object.entries(lockPackages)) {
38
+ if (path === '')
39
+ continue; // Skip root
40
+ if (path.startsWith('node_modules/')) {
41
+ const name = path.split('node_modules/').pop();
42
+ // If we have multiple paths for the same package (transitive drift),
43
+ // we'll handle them as separate nodes if their versions differ,
44
+ // but usually buildDependencyGraph has been flat-ish so far.
45
+ // Let's keep it simple: one node per unique package name from the lockfile's perspective.
46
+ const versionRange = directDeps[name] || pkg.version;
47
+ const isTransitive = !directDeps[name];
48
+ const meta = metadataMap[name];
49
+ // Direct parents are those who have this dependency in their path segment
50
+ // but we filter out 'root' for the introducedBy array as requested previously
51
+ // actually, if it's transitive, we want the non-root parents.
52
+ const parents = Array.from(parentMap[name] || []).filter(p => p !== (packageJson.name || 'root'));
53
+ dependencies.push({
54
+ name,
55
+ version: typeof versionRange === 'string' ? versionRange : pkg.version,
56
+ resolvedVersion: pkg.version,
57
+ transitive: isTransitive,
58
+ introducedBy: parents,
59
+ metadata: {
60
+ lastPublish: meta?.time?.[meta['dist-tags']?.latest],
61
+ maintainers: meta?.maintainers?.length,
62
+ deprecated: meta?.deprecated,
63
+ description: meta?.description,
64
+ },
65
+ });
66
+ }
67
+ }
68
+ return {
69
+ root: packageJson.name || 'root',
70
+ environment,
71
+ dependencies,
72
+ };
73
+ }
@@ -0,0 +1,78 @@
1
+ export function analyzeSecurity(graph) {
2
+ const groups = [];
3
+ const now = new Date();
4
+ const eighteenMonthsAgo = new Date();
5
+ eighteenMonthsAgo.setMonth(now.getMonth() - 18);
6
+ // We check each unique dependency name in the graph
7
+ const dependencyMap = new Map();
8
+ for (const node of graph.dependencies) {
9
+ if (dependencyMap.has(node.name))
10
+ continue;
11
+ const { metadata } = node;
12
+ const issues = [];
13
+ // 1. Deprecated
14
+ if (metadata.deprecated) {
15
+ issues.push({
16
+ type: 'deprecated',
17
+ reason: `The author of "${node.name}" has marked this library as obsolete (deprecated). Message: "${metadata.deprecated}". It is highly recommended to find a modern alternative as it will likely no longer receive updates.`,
18
+ riskLevel: 'high',
19
+ details: {
20
+ message: metadata.deprecated,
21
+ latestVersion: node.resolvedVersion,
22
+ description: metadata.description
23
+ }
24
+ });
25
+ }
26
+ // 2. Unmaintained
27
+ if (metadata.lastPublish) {
28
+ const lastPublishDate = new Date(metadata.lastPublish);
29
+ if (lastPublishDate < eighteenMonthsAgo) {
30
+ const monthsSince = Math.round((now.getTime() - lastPublishDate.getTime()) / (1000 * 60 * 60 * 24 * 30.44));
31
+ issues.push({
32
+ type: 'unmaintained',
33
+ reason: `The last update for "${node.name}" was on ${lastPublishDate.toLocaleDateString()} (more than 18 months ago). A library that is no longer updated may contain unpatched security vulnerabilities or become incompatible with newer Node.js versions.`,
34
+ riskLevel: 'medium',
35
+ details: {
36
+ lastUpdate: lastPublishDate.toISOString().split('T')[0],
37
+ monthsSinceLastUpdate: monthsSince,
38
+ version: node.resolvedVersion,
39
+ description: metadata.description
40
+ }
41
+ });
42
+ }
43
+ }
44
+ // 3. Single maintainer
45
+ if (metadata.maintainers === 1) {
46
+ issues.push({
47
+ type: 'single-maintainer',
48
+ reason: `"${node.name}" is managed by only one person. This is risky because if this person stops maintaining it or if their account is compromised, there is no one else to fix issues quickly.`,
49
+ riskLevel: 'low',
50
+ details: {
51
+ maintainerCount: 1,
52
+ description: metadata.description
53
+ }
54
+ });
55
+ }
56
+ if (issues.length > 0) {
57
+ // Calculate overallRisk
58
+ const riskOrder = { 'low': 0, 'medium': 1, 'high': 2 };
59
+ let overallRisk = 'low';
60
+ for (const issue of issues) {
61
+ if (riskOrder[issue.riskLevel] > riskOrder[overallRisk]) {
62
+ overallRisk = issue.riskLevel;
63
+ }
64
+ }
65
+ const group = {
66
+ dependencyName: node.name,
67
+ transitive: node.transitive,
68
+ introducedBy: node.introducedBy || [],
69
+ description: metadata.description,
70
+ issues,
71
+ overallRisk
72
+ };
73
+ dependencyMap.set(node.name, group);
74
+ groups.push(group);
75
+ }
76
+ }
77
+ return groups;
78
+ }
@@ -0,0 +1,44 @@
1
+ import { z } from 'zod';
2
+ export const RiskLevelSchema = z.enum(['low', 'medium', 'high']);
3
+ export const RecommendedActionSchema = z.enum(['allow', 'warn', 'block']);
4
+ export const DependencyNodeSchema = z.object({
5
+ name: z.string(),
6
+ version: z.string(), // Range or fixed version from package.json
7
+ resolvedVersion: z.string(), // Actual version from lockfile
8
+ transitive: z.boolean(),
9
+ introducedBy: z.array(z.string()).optional(),
10
+ metadata: z.object({
11
+ lastPublish: z.string().optional(), // Date string
12
+ maintainers: z.number().optional(),
13
+ downloads: z.number().optional(),
14
+ deprecated: z.string().optional(),
15
+ description: z.string().optional(),
16
+ }),
17
+ });
18
+ export const DependencyGraphSchema = z.object({
19
+ root: z.string(),
20
+ environment: z.enum(['local', 'ci', 'prod']),
21
+ dependencies: z.array(DependencyNodeSchema),
22
+ });
23
+ export const ScanMetadataSchema = z.object({
24
+ schemaVersion: z.literal("1.0"),
25
+ scanId: z.string().uuid(),
26
+ projectName: z.string(),
27
+ projectId: z.string(),
28
+ generatedAt: z.string().datetime(),
29
+ });
30
+ export const AnalysisReportSchema = z.object({
31
+ summary: z.object({
32
+ driftCount: z.number(),
33
+ securityCount: z.number(),
34
+ riskLevel: RiskLevelSchema,
35
+ riskReason: z.string().optional(),
36
+ recommendedAction: RecommendedActionSchema.optional(),
37
+ recommendedExitCode: z.number(),
38
+ }),
39
+ drift: z.array(z.any()),
40
+ security: z.array(z.any()),
41
+ });
42
+ export const ScanResultSchema = AnalysisReportSchema.extend({
43
+ meta: ScanMetadataSchema,
44
+ });
@@ -0,0 +1,69 @@
1
+ export function formatConsoleReport(report) {
2
+ const { summary, drift, security } = report;
3
+ let output = `\n=== dep-drift-sec Analysis ===\n`;
4
+ output += `Risk Level: ${summary.riskLevel.toUpperCase()}\n`;
5
+ output += `Action: ${(summary.recommendedAction || 'allow').toUpperCase()}\n`;
6
+ output += `Reason: ${summary.riskReason || 'N/A'}\n\n`;
7
+ output += `Drift Issues: ${summary.driftCount}\n`;
8
+ output += `Security Issues: ${summary.securityCount}\n\n`;
9
+ if (drift.length > 0) {
10
+ output += `--- Dependency Drift ---\n`;
11
+ drift.forEach((group) => {
12
+ const label = group.transitive ? '[TRANSITIVE]' : '[DIRECT]';
13
+ const introducedBy = group.transitive && group.introducedBy.length > 0
14
+ ? ` (via ${group.introducedBy.join(', ')})`
15
+ : '';
16
+ output += `${label} ${group.dependencyName}${introducedBy}\n`;
17
+ output += ` Impact: This dependency has version fluctuations which can lead to "works on my machine" bugs.\n`;
18
+ group.issues.forEach(issue => {
19
+ output += ` - [${issue.type.toUpperCase()}] ${issue.reason}\n`;
20
+ output += ` Expected: ${issue.expected}, Actual: ${issue.actual}\n`;
21
+ });
22
+ output += `\n`;
23
+ });
24
+ }
25
+ if (security.length > 0) {
26
+ output += `--- Security Heuristics ---\n`;
27
+ security.forEach((group) => {
28
+ const label = group.transitive ? '[TRANSITIVE]' : '[DIRECT]';
29
+ const introducedBy = group.transitive && group.introducedBy.length > 0
30
+ ? ` (via ${group.introducedBy.join(', ')})`
31
+ : '';
32
+ output += `[${group.overallRisk.toUpperCase()}] ${label} ${group.dependencyName}${introducedBy}\n`;
33
+ // Per-dependency explanation
34
+ if (group.overallRisk === 'high') {
35
+ output += ` Impact: This package is deprecated or critical; it should be replaced immediately to avoid security breaches.\n`;
36
+ }
37
+ else if (group.overallRisk === 'medium') {
38
+ output += ` Impact: This package is unmaintained; it may have hidden vulnerabilities or compatibility issues.\n`;
39
+ }
40
+ else {
41
+ output += ` Impact: This package has minor supply chain risks (e.g., single maintainer).\n`;
42
+ }
43
+ if (group.description) {
44
+ output += ` Description: ${group.description}\n`;
45
+ }
46
+ group.issues.forEach(issue => {
47
+ output += ` - [${issue.type.toUpperCase()}] ${issue.reason}\n`;
48
+ if (issue.details) {
49
+ const detailLines = [];
50
+ for (const [key, value] of Object.entries(issue.details)) {
51
+ if (key === 'description')
52
+ continue;
53
+ if (value !== undefined)
54
+ detailLines.push(`${key}: ${value}`);
55
+ }
56
+ if (detailLines.length > 0) {
57
+ output += ` Details: ${detailLines.join(', ')}\n`;
58
+ }
59
+ }
60
+ });
61
+ output += `\n`;
62
+ });
63
+ }
64
+ if (summary.driftCount === 0 && summary.securityCount === 0) {
65
+ output += `No issues detected. Your dependencies are healthy!\n`;
66
+ }
67
+ output += `\nRecommended Exit Code: ${summary.recommendedExitCode}\n`;
68
+ return output;
69
+ }
@@ -0,0 +1,6 @@
1
+ import { ScanResultSchema } from '../core/types.js';
2
+ export function formatJsonReport(report) {
3
+ // Validate schema before outputting to ensure stability
4
+ ScanResultSchema.parse(report);
5
+ return JSON.stringify(report, null, 2);
6
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "dep-drift-sec",
3
+ "version": "0.1.0",
4
+ "main": "index.js",
5
+ "scripts": {
6
+ "test": "vitest",
7
+ "build": "tsc",
8
+ "scan": "node dist/cli/index.js check"
9
+ },
10
+ "keywords": [],
11
+ "author": "",
12
+ "license": "MIT",
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/simonelakra/dep-drift-sec.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/simonelakra/dep-drift-sec/issues"
22
+ },
23
+ "homepage": "https://github.com/simonelakra/dep-drift-sec#readme",
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "description": "Production-ready CLI to detect dependency drift and security risks.",
28
+ "devDependencies": {
29
+ "@types/node": "^25.0.9",
30
+ "tsx": "^4.21.0",
31
+ "typescript": "^5.9.3",
32
+ "vitest": "^4.0.17"
33
+ },
34
+ "dependencies": {
35
+ "axios": "^1.13.2",
36
+ "commander": "^14.0.2",
37
+ "zod": "^4.3.5"
38
+ },
39
+ "bin": {
40
+ "dep-drift-sec": "dist/cli/index.js"
41
+ },
42
+ "type": "module"
43
+ }