@lateos/npm-scan 0.16.0 → 0.16.5

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.
Files changed (110) hide show
  1. package/.dockerignore +20 -20
  2. package/.husky/pre-commit +1 -1
  3. package/CHANGELOG.md +199 -199
  4. package/LICENSING.md +19 -19
  5. package/README.de.md +708 -669
  6. package/README.fr.md +707 -668
  7. package/README.ja.md +704 -665
  8. package/README.md +826 -801
  9. package/README.zh.md +708 -669
  10. package/SECURITY.md +72 -72
  11. package/backend/cra.js +68 -68
  12. package/backend/db/schema.sql +32 -32
  13. package/backend/db.js +88 -88
  14. package/backend/detectors/atk-001-lifecycle.js +17 -17
  15. package/backend/detectors/atk-002-obfusc.js +261 -261
  16. package/backend/detectors/atk-003-creds.js +13 -13
  17. package/backend/detectors/atk-004-persist.js +13 -13
  18. package/backend/detectors/atk-005-exfil.js +13 -13
  19. package/backend/detectors/atk-006-depconf.js +14 -14
  20. package/backend/detectors/atk-007-typosquat.js +34 -34
  21. package/backend/detectors/atk-008-tarball-tamper.js +91 -91
  22. package/backend/detectors/atk-009-dormant-trigger.js +62 -62
  23. package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
  24. package/backend/detectors/atk-011-transitive-prop.js +76 -76
  25. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +24 -0
  26. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +24 -0
  27. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +90 -0
  28. package/backend/detectors/axios-poisoning/index.js +94 -0
  29. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
  30. package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
  31. package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
  32. package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
  33. package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
  34. package/backend/detectors/hf-impersonation/index.js +396 -396
  35. package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
  36. package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
  37. package/backend/detectors/hf-impersonation/simhash.js +46 -46
  38. package/backend/detectors/index.js +75 -38
  39. package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
  40. package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
  41. package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
  42. package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
  43. package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
  44. package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
  45. package/backend/detectors/megalodon/index.js +80 -80
  46. package/backend/detectors/megalodon/types.js +9 -9
  47. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
  48. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
  49. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
  50. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
  51. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
  52. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
  53. package/backend/detectors/mini-shai-hulud/index.js +118 -118
  54. package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
  55. package/backend/detectors/msh-supplement/d1-obfuscation.js +18 -0
  56. package/backend/detectors/msh-supplement/d2-persistence.js +47 -0
  57. package/backend/detectors/msh-supplement/d3-geo-killswitch.js +35 -0
  58. package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +33 -0
  59. package/backend/detectors/msh-supplement/index.js +107 -0
  60. package/backend/detectors/tier1-binary-embed.js +219 -0
  61. package/backend/detectors/tier1-infostealer.js +280 -0
  62. package/backend/detectors/tier1-lifecycle-hook.js +176 -0
  63. package/backend/detectors/tier1-metadata-spoof.js +180 -0
  64. package/backend/detectors/tier1-typosquat.js +219 -0
  65. package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +77 -0
  66. package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +37 -0
  67. package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +66 -0
  68. package/backend/detectors/typosquat-vpmdhaj/index.js +98 -0
  69. package/backend/fetch.js +175 -175
  70. package/backend/index.js +4 -4
  71. package/backend/license.js +89 -89
  72. package/backend/lockfile.js +379 -379
  73. package/backend/pdf.js +245 -245
  74. package/backend/policy.js +193 -176
  75. package/backend/provenance.js +79 -0
  76. package/backend/report.js +254 -254
  77. package/backend/sbom.js +66 -66
  78. package/backend/siem/cef.js +32 -32
  79. package/backend/siem/ecs.js +40 -40
  80. package/backend/siem/index.js +18 -18
  81. package/backend/siem/qradar.js +56 -56
  82. package/backend/siem/sentinel.js +27 -27
  83. package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
  84. package/backend/vsix-scan/detectors/burst-publish.js +52 -52
  85. package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
  86. package/backend/vsix-scan/detectors/known-ioc.js +105 -105
  87. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
  88. package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
  89. package/backend/vsix-scan/index.js +183 -183
  90. package/backend/vsix-scan/marketplace-client.js +145 -145
  91. package/backend/vsix-scan/vsix-iocs.json +31 -31
  92. package/cli/cli.js +458 -458
  93. package/deploy/helm/npm-scan/Chart.yaml +21 -21
  94. package/deploy/helm/npm-scan/templates/_helpers.tpl +8 -8
  95. package/deploy/helm/npm-scan/templates/api.yaml +93 -93
  96. package/deploy/helm/npm-scan/templates/ingress.yaml +27 -27
  97. package/deploy/helm/npm-scan/templates/postgresql.yaml +66 -66
  98. package/deploy/helm/npm-scan/templates/secrets.yaml +18 -18
  99. package/deploy/helm/npm-scan/templates/worker.yaml +31 -31
  100. package/deploy/helm/npm-scan/values.byoc.yaml +74 -74
  101. package/deploy/helm/npm-scan/values.yaml +102 -102
  102. package/package.json +57 -57
  103. package/scripts/download-corpus.js +30 -30
  104. package/scripts/gen-mal-corpus.js +34 -34
  105. package/scripts/generate-campaign-fixtures.js +170 -0
  106. package/src/config/top-5000.json +87 -0
  107. package/test/fixtures/lockfiles/npm-lock.json +68 -68
  108. package/test/fixtures/lockfiles/pnpm-lock.yaml +117 -117
  109. package/test/fixtures/lockfiles/yarn.lock +103 -103
  110. package/test/fixtures/mock-data.js +69 -69
package/backend/policy.js CHANGED
@@ -1,176 +1,193 @@
1
- import { readFileSync } from 'fs';
2
- import { load as yamlLoad } from 'js-yaml';
3
-
4
- const SEVERITY_ORDER = ['none', 'low', 'medium', 'high', 'critical'];
5
- const VALID_SEVERITIES = new Set(SEVERITY_ORDER);
6
-
7
- const KNOWN_REPUTABLE_PACKAGES = new Set([
8
- 'react', 'react-dom', 'vue', 'angular', 'next', 'nuxt',
9
- 'express', 'fastify', 'hono', 'koa', 'connect',
10
- 'webpack', 'vite', 'rollup', 'esbuild', 'typescript', 'babel-core',
11
- 'lodash', 'ramda', 'underscore',
12
- 'axios', 'node-fetch', 'got', 'superagent',
13
- 'sequelize', 'prisma', 'typeorm', 'mongoose',
14
- 'jest', 'mocha', 'vitest', 'ava',
15
- 'prettier', 'eslint', 'stylelint',
16
- 'socket.io', 'ws',
17
- 'rimraf', 'glob', 'minimatch', 'fs-extra',
18
- ]);
19
-
20
- function severityIndex(s) {
21
- return SEVERITY_ORDER.indexOf(s);
22
- }
23
-
24
- function matchesFilePath(filePath, pattern) {
25
- if (!pattern) return false;
26
- if (pattern === '*') return true;
27
- const regexPattern = pattern
28
- .replace(/\./g, '\\.')
29
- .replace(/\*\*/g, '___DOUBLE_STAR___')
30
- .replace(/\*/g, '[^/]*')
31
- .replace(/___DOUBLE_STAR___/g, '.*');
32
- return new RegExp(`^${regexPattern}$`).test(filePath);
33
- }
34
-
35
- function matchesContext(finding, rule) {
36
- const ctx = finding.context;
37
- if (!ctx) return false;
38
-
39
- if (rule.context?.is_dist_build === true && !ctx.is_dist_build) return false;
40
- if (rule.context?.is_dist_build === false && ctx.is_dist_build) return false;
41
- if (rule.context?.is_test_fixture === true && !ctx.is_test_fixture) return false;
42
- if (rule.context?.is_test_fixture === false && ctx.is_test_fixture) return false;
43
- if (rule.context?.is_lifecycle_hook === true && !ctx.is_lifecycle_hook) return false;
44
- if (rule.context?.is_lifecycle_hook === false && ctx.is_lifecycle_hook) return false;
45
- if (rule.context?.is_known_safe_domain === true && !ctx.is_known_safe_domain) return false;
46
- if (rule.context?.is_known_safe_domain === false && ctx.is_known_safe_domain) return false;
47
-
48
- if (rule.context?.file_path && !matchesFilePath(ctx.file_path, rule.context.file_path)) return false;
49
- if (rule.context?.url_domain) {
50
- if (!ctx.url_domain) return false;
51
- const domainPattern = rule.context.url_domain.replace(/\*/g, '.*');
52
- if (!new RegExp(`^${domainPattern}$`).test(ctx.url_domain)) return false;
53
- }
54
-
55
- return true;
56
- }
57
-
58
- function getPackageReputationTier(pkgName) {
59
- const name = pkgName?.replace(/^@/, '').replace(/\/.*/, '') || '';
60
- if (KNOWN_REPUTABLE_PACKAGES.has(name)) return 'trusted';
61
- return 'unknown';
62
- }
63
-
64
- function matchesSuppressRule(finding, pkgName, rule) {
65
- if (rule.atk_id !== (finding.atk_id || finding.id)) return false;
66
- if (rule.package && rule.package !== '*' && rule.package !== pkgName) return false;
67
-
68
- if (rule.context && !matchesContext(finding, rule)) return false;
69
-
70
- if (rule.reputation_tier) {
71
- const tier = getPackageReputationTier(pkgName);
72
- if (rule.reputation_tier !== tier && !(rule.reputation_tier === '*' || rule.reputation_tier === 'any')) return false;
73
- }
74
-
75
- return true;
76
- }
77
-
78
- function loadPolicy(path) {
79
- const raw = readFileSync(path, 'utf8').trim();
80
- let policy;
81
-
82
- if (path.endsWith('.json')) {
83
- policy = JSON.parse(raw);
84
- } else {
85
- policy = yamlLoad(raw);
86
- }
87
-
88
- if (!policy || typeof policy !== 'object') {
89
- throw new Error('Policy file must contain a valid YAML/JSON object');
90
- }
91
-
92
- if (policy.severity_overrides) {
93
- for (const [atkId, severity] of Object.entries(policy.severity_overrides)) {
94
- if (!VALID_SEVERITIES.has(severity)) {
95
- throw new Error(`Invalid severity "${severity}" for ${atkId} — must be one of: low, medium, high, critical`);
96
- }
97
- }
98
- }
99
-
100
- if (policy.fail_on && !VALID_SEVERITIES.has(policy.fail_on)) {
101
- throw new Error(`Invalid fail_on "${policy.fail_on}" — must be one of: none, low, medium, high, critical`);
102
- }
103
-
104
- if (policy.suppress) {
105
- if (!Array.isArray(policy.suppress)) {
106
- throw new Error('suppress must be an array');
107
- }
108
- for (const rule of policy.suppress) {
109
- if (!rule.atk_id) {
110
- throw new Error('Each suppress rule must have an atk_id');
111
- }
112
- }
113
- }
114
-
115
- if (policy.allow) {
116
- if (policy.allow.packages && !Array.isArray(policy.allow.packages)) {
117
- throw new Error('allow.packages must be an array');
118
- }
119
- }
120
-
121
- return sanitizePolicy(policy);
122
- }
123
-
124
- function sanitizePolicy(policy) {
125
- return {
126
- allow: { packages: policy.allow?.packages ?? [] },
127
- severity_overrides: policy.severity_overrides ?? {},
128
- fail_on: policy.fail_on ?? 'none',
129
- suppress: (policy.suppress ?? []).map(r => ({
130
- atk_id: r.atk_id,
131
- package: r.package || '*',
132
- reason: r.reason || '',
133
- context: r.context || null,
134
- reputation_tier: r.reputation_tier || null,
135
- })),
136
- };
137
- }
138
-
139
- function isAllowed(packageName, policy) {
140
- if (!policy.allow.packages.length) return false;
141
- const nameOnly = packageName.split('@')[0];
142
- return policy.allow.packages.some(p => p === packageName || p === nameOnly);
143
- }
144
-
145
- function applyPolicy(findings, packageName, policy) {
146
- let filtered = [...findings];
147
-
148
- if (policy.suppress.length) {
149
- filtered = filtered.filter(f => {
150
- if (f.context?.is_lifecycle_hook) return true;
151
- if (f.context?.is_multi_layer) return true;
152
- return !policy.suppress.some(r => matchesSuppressRule(f, packageName, r));
153
- });
154
- }
155
-
156
- filtered = filtered.map(f => {
157
- const override = policy.severity_overrides[f.atk_id || f.id];
158
- if (override) {
159
- return { ...f, severity: override, _severityOverridden: true };
160
- }
161
- return f;
162
- });
163
-
164
- const blocked = checkFailOn(filtered, policy);
165
-
166
- return { findings: filtered, blocked };
167
- }
168
-
169
- function checkFailOn(findings, policy) {
170
- if (policy.fail_on === 'none') return false;
171
-
172
- const threshold = severityIndex(policy.fail_on);
173
- return findings.some(f => severityIndex(f.severity) >= threshold);
174
- }
175
-
176
- export { loadPolicy, applyPolicy, isAllowed, getPackageReputationTier, matchesContext };
1
+ import { readFileSync } from 'fs';
2
+ import { load as yamlLoad } from 'js-yaml';
3
+
4
+ const SEVERITY_ORDER = ['none', 'low', 'medium', 'high', 'critical'];
5
+ const VALID_SEVERITIES = new Set(SEVERITY_ORDER);
6
+
7
+ const KNOWN_REPUTABLE_PACKAGES = new Set([
8
+ 'react', 'react-dom', 'vue', 'angular', 'next', 'nuxt',
9
+ 'express', 'fastify', 'hono', 'koa', 'connect',
10
+ 'webpack', 'vite', 'rollup', 'esbuild', 'typescript', 'babel-core',
11
+ 'lodash', 'ramda', 'underscore',
12
+ 'axios', 'node-fetch', 'got', 'superagent',
13
+ 'sequelize', 'prisma', 'typeorm', 'mongoose',
14
+ 'jest', 'mocha', 'vitest', 'ava',
15
+ 'prettier', 'eslint', 'stylelint',
16
+ 'socket.io', 'ws',
17
+ 'rimraf', 'glob', 'minimatch', 'fs-extra',
18
+ 'electron', 'puppeteer', 'playwright', 'sharp', 'node-canvas',
19
+ 'ffmpeg-static', 'turbo',
20
+ 'react-scripts', '@angular/cli',
21
+ 'gatsby', 'parcel',
22
+ 'tslib', 'core-js', 'regenerator-runtime', 'buffer',
23
+ 'node-gyp', 'node-pre-gyp',
24
+ 'winston', 'uuid', 'moment', 'dotenv', 'pg', 'semver', 'redux', 'redis',
25
+ 'dayjs', 'luxon', 'chalk', 'debug', 'cors', 'helmet', 'multer',
26
+ 'body-parser', 'cheerio', 'bluebird', 'bcrypt', 'commander', 'yargs',
27
+ 'passport', 'jsonwebtoken', 'nodemailer', 'class-validator',
28
+ ]);
29
+
30
+ function severityIndex(s) {
31
+ return SEVERITY_ORDER.indexOf(s);
32
+ }
33
+
34
+ function matchesFilePath(filePath, pattern) {
35
+ if (!pattern) return false;
36
+ if (pattern === '*') return true;
37
+ const regexPattern = pattern
38
+ .replace(/\./g, '\\.')
39
+ .replace(/\*\*/g, '___DOUBLE_STAR___')
40
+ .replace(/\*/g, '[^/]*')
41
+ .replace(/___DOUBLE_STAR___/g, '.*');
42
+ return new RegExp(`^${regexPattern}$`).test(filePath);
43
+ }
44
+
45
+ function matchesContext(finding, rule) {
46
+ const ctx = finding.context;
47
+ if (!ctx) return false;
48
+
49
+ if (rule.context?.is_dist_build === true && !ctx.is_dist_build) return false;
50
+ if (rule.context?.is_dist_build === false && ctx.is_dist_build) return false;
51
+ if (rule.context?.is_test_fixture === true && !ctx.is_test_fixture) return false;
52
+ if (rule.context?.is_test_fixture === false && ctx.is_test_fixture) return false;
53
+ if (rule.context?.is_lifecycle_hook === true && !ctx.is_lifecycle_hook) return false;
54
+ if (rule.context?.is_lifecycle_hook === false && ctx.is_lifecycle_hook) return false;
55
+ if (rule.context?.is_known_safe_domain === true && !ctx.is_known_safe_domain) return false;
56
+ if (rule.context?.is_known_safe_domain === false && ctx.is_known_safe_domain) return false;
57
+
58
+ if (rule.context?.file_path && !matchesFilePath(ctx.file_path, rule.context.file_path)) return false;
59
+ if (rule.context?.url_domain) {
60
+ if (!ctx.url_domain) return false;
61
+ const domainPattern = rule.context.url_domain.replace(/\*/g, '.*');
62
+ if (!new RegExp(`^${domainPattern}$`).test(ctx.url_domain)) return false;
63
+ }
64
+
65
+ return true;
66
+ }
67
+
68
+ function matchesKnownReputable(packageName) {
69
+ if (KNOWN_REPUTABLE_PACKAGES.has(packageName)) return true;
70
+ const [scope, name] = packageName.split('/');
71
+ if (scope && name && KNOWN_REPUTABLE_PACKAGES.has(`${scope}/*`)) return true;
72
+ return false;
73
+ }
74
+
75
+ function getPackageReputationTier(pkgName) {
76
+ const name = pkgName?.replace(/^@/, '').replace(/\/.*/, '') || '';
77
+ if (matchesKnownReputable(name)) return 'trusted';
78
+ return 'unknown';
79
+ }
80
+
81
+ function matchesSuppressRule(finding, pkgName, rule) {
82
+ if (rule.atk_id !== (finding.atk_id || finding.id)) return false;
83
+ if (rule.package && rule.package !== '*' && rule.package !== pkgName) return false;
84
+
85
+ if (rule.context && !matchesContext(finding, rule)) return false;
86
+
87
+ if (rule.reputation_tier) {
88
+ const tier = getPackageReputationTier(pkgName);
89
+ if (rule.reputation_tier !== tier && !(rule.reputation_tier === '*' || rule.reputation_tier === 'any')) return false;
90
+ }
91
+
92
+ return true;
93
+ }
94
+
95
+ function loadPolicy(path) {
96
+ const raw = readFileSync(path, 'utf8').trim();
97
+ let policy;
98
+
99
+ if (path.endsWith('.json')) {
100
+ policy = JSON.parse(raw);
101
+ } else {
102
+ policy = yamlLoad(raw);
103
+ }
104
+
105
+ if (!policy || typeof policy !== 'object') {
106
+ throw new Error('Policy file must contain a valid YAML/JSON object');
107
+ }
108
+
109
+ if (policy.severity_overrides) {
110
+ for (const [atkId, severity] of Object.entries(policy.severity_overrides)) {
111
+ if (!VALID_SEVERITIES.has(severity)) {
112
+ throw new Error(`Invalid severity "${severity}" for ${atkId} — must be one of: low, medium, high, critical`);
113
+ }
114
+ }
115
+ }
116
+
117
+ if (policy.fail_on && !VALID_SEVERITIES.has(policy.fail_on)) {
118
+ throw new Error(`Invalid fail_on "${policy.fail_on}" — must be one of: none, low, medium, high, critical`);
119
+ }
120
+
121
+ if (policy.suppress) {
122
+ if (!Array.isArray(policy.suppress)) {
123
+ throw new Error('suppress must be an array');
124
+ }
125
+ for (const rule of policy.suppress) {
126
+ if (!rule.atk_id) {
127
+ throw new Error('Each suppress rule must have an atk_id');
128
+ }
129
+ }
130
+ }
131
+
132
+ if (policy.allow) {
133
+ if (policy.allow.packages && !Array.isArray(policy.allow.packages)) {
134
+ throw new Error('allow.packages must be an array');
135
+ }
136
+ }
137
+
138
+ return sanitizePolicy(policy);
139
+ }
140
+
141
+ function sanitizePolicy(policy) {
142
+ return {
143
+ allow: { packages: policy.allow?.packages ?? [] },
144
+ severity_overrides: policy.severity_overrides ?? {},
145
+ fail_on: policy.fail_on ?? 'none',
146
+ suppress: (policy.suppress ?? []).map(r => ({
147
+ atk_id: r.atk_id,
148
+ package: r.package || '*',
149
+ reason: r.reason || '',
150
+ context: r.context || null,
151
+ reputation_tier: r.reputation_tier || null,
152
+ })),
153
+ };
154
+ }
155
+
156
+ function isAllowed(packageName, policy) {
157
+ if (!policy.allow.packages.length) return false;
158
+ const nameOnly = packageName.split('@')[0];
159
+ return policy.allow.packages.some(p => p === packageName || p === nameOnly);
160
+ }
161
+
162
+ function applyPolicy(findings, packageName, policy) {
163
+ let filtered = [...findings];
164
+
165
+ if (policy.suppress.length) {
166
+ filtered = filtered.filter(f => {
167
+ if (f.context?.is_lifecycle_hook) return true;
168
+ if (f.context?.is_multi_layer) return true;
169
+ return !policy.suppress.some(r => matchesSuppressRule(f, packageName, r));
170
+ });
171
+ }
172
+
173
+ filtered = filtered.map(f => {
174
+ const override = policy.severity_overrides[f.atk_id || f.id];
175
+ if (override) {
176
+ return { ...f, severity: override, _severityOverridden: true };
177
+ }
178
+ return f;
179
+ });
180
+
181
+ const blocked = checkFailOn(filtered, policy);
182
+
183
+ return { findings: filtered, blocked };
184
+ }
185
+
186
+ function checkFailOn(findings, policy) {
187
+ if (policy.fail_on === 'none') return false;
188
+
189
+ const threshold = severityIndex(policy.fail_on);
190
+ return findings.some(f => severityIndex(f.severity) >= threshold);
191
+ }
192
+
193
+ export { loadPolicy, applyPolicy, isAllowed, getPackageReputationTier, matchesContext, KNOWN_REPUTABLE_PACKAGES };
@@ -0,0 +1,79 @@
1
+ import { createHash, createHmac } from 'crypto';
2
+
3
+ const PROVENANCE_VERSION = 'aureus-v1.7';
4
+
5
+ const HMAC_KEY = process.env.AUREUS_HMAC_KEY || '@lateos/npm-scan:provenance:v1';
6
+
7
+ export function hashContent(content) {
8
+ return createHash('sha256').update(JSON.stringify(content)).digest('hex');
9
+ }
10
+
11
+ export function signManifest(manifest, key = HMAC_KEY) {
12
+ return createHmac('sha256', key).update(JSON.stringify(manifest)).digest('hex');
13
+ }
14
+
15
+ export function buildDetectionRule({ ruleId, ruleName, severity, cveReferences = [], campaignName }) {
16
+ return {
17
+ rule_id: ruleId,
18
+ rule_name: ruleName,
19
+ severity,
20
+ cve_references: cveReferences,
21
+ campaign_name: campaignName,
22
+ };
23
+ }
24
+
25
+ export function buildScanMetadata({ scannerVersion, packageAnalyzed }) {
26
+ return {
27
+ scan_timestamp: new Date().toISOString(),
28
+ scanner_version: scannerVersion || '@lateos/npm-scan',
29
+ pipeline_version: PROVENANCE_VERSION,
30
+ package_analyzed: packageAnalyzed,
31
+ };
32
+ }
33
+
34
+ export function buildDetectionResult({ triggered, severity, indicators = [] }) {
35
+ return {
36
+ triggered,
37
+ severity,
38
+ indicators,
39
+ };
40
+ }
41
+
42
+ export function buildAuditTrail({ detectionLogic, ruleProvenanceUrl, campaignSourceUrl }) {
43
+ const contentHash = hashContent(detectionLogic);
44
+ const manifest = { contentHash, ruleProvenanceUrl, campaignSourceUrl, generatedAt: new Date().toISOString() };
45
+ return {
46
+ content_hash: contentHash,
47
+ rule_provenance_url: ruleProvenanceUrl,
48
+ campaign_source_url: campaignSourceUrl,
49
+ hmac_signature: signManifest(manifest),
50
+ _manifest: manifest,
51
+ };
52
+ }
53
+
54
+ export function buildDetectionRecord({ rule, scanMetadata, detectionResult, auditTrail }) {
55
+ return {
56
+ detection_rule: rule,
57
+ scan_metadata: scanMetadata,
58
+ detection_result: detectionResult,
59
+ audit_trail: auditTrail,
60
+ };
61
+ }
62
+
63
+ export function attachProvenance(evidence, { ruleId, ruleName, severity, campaignName, pkgName, pkgVersion, triggered, indicators, ruleProvenanceUrl, campaignSourceUrl }) {
64
+ const rule = buildDetectionRule({ ruleId, ruleName, severity, campaignName });
65
+ const scanMetadata = buildScanMetadata({
66
+ scannerVersion: '@lateos/npm-scan',
67
+ packageAnalyzed: `${pkgName}@${pkgVersion}`,
68
+ });
69
+ const detectionResult = buildDetectionResult({ triggered, severity, indicators });
70
+ const auditTrail = buildAuditTrail({
71
+ detectionLogic: { rule, indicators },
72
+ ruleProvenanceUrl,
73
+ campaignSourceUrl,
74
+ });
75
+ const record = buildDetectionRecord({ rule, scanMetadata, detectionResult, auditTrail });
76
+ return { ...evidence, _provenance: record };
77
+ }
78
+
79
+ export { PROVENANCE_VERSION };