@lateos/npm-scan 1.0.0 → 1.1.1

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 (129) hide show
  1. package/README.de.md +3 -98
  2. package/README.fr.md +3 -98
  3. package/README.ja.md +3 -98
  4. package/README.md +2 -122
  5. package/README.zh.md +3 -98
  6. package/backend/cra.js +113 -21
  7. package/backend/db.js +18 -10
  8. package/backend/detectors/atk-001-lifecycle.js +5 -5
  9. package/backend/detectors/atk-002-obfusc.js +126 -47
  10. package/backend/detectors/atk-003-creds.js +8 -4
  11. package/backend/detectors/atk-004-persist.js +3 -3
  12. package/backend/detectors/atk-005-exfil.js +8 -4
  13. package/backend/detectors/atk-006-depconf.js +3 -3
  14. package/backend/detectors/atk-007-typosquat.js +64 -10
  15. package/backend/detectors/atk-008-tarball-tamper.js +6 -6
  16. package/backend/detectors/atk-009-dormant-trigger.js +9 -5
  17. package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
  18. package/backend/detectors/atk-011-transitive-prop.js +14 -13
  19. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
  20. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
  21. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
  22. package/backend/detectors/axios-poisoning/index.js +77 -60
  23. package/backend/detectors/config/thresholds.js +48 -3
  24. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
  25. package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
  26. package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
  27. package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
  28. package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
  29. package/backend/detectors/hf-impersonation/index.js +94 -31
  30. package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
  31. package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
  32. package/backend/detectors/hf-impersonation/simhash.js +2 -2
  33. package/backend/detectors/index.js +181 -34
  34. package/backend/detectors/lib/ast-patterns.js +4 -1
  35. package/backend/detectors/lib/entropy-analyzer.js +12 -4
  36. package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
  37. package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
  38. package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
  39. package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
  40. package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
  41. package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
  42. package/backend/detectors/megalodon/index.js +35 -25
  43. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
  44. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
  45. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
  46. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
  47. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
  48. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
  49. package/backend/detectors/mini-shai-hulud/index.js +63 -26
  50. package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
  51. package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
  52. package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
  53. package/backend/detectors/msh-supplement/index.js +78 -63
  54. package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
  55. package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
  56. package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
  57. package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
  58. package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
  59. package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
  60. package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
  61. package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
  62. package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
  63. package/backend/detectors/node-ipc-compromise/index.js +21 -15
  64. package/backend/detectors/tier1-binary-embed.js +109 -41
  65. package/backend/detectors/tier1-cloud-imds.js +57 -37
  66. package/backend/detectors/tier1-encrypted-c2.js +198 -0
  67. package/backend/detectors/tier1-infostealer.js +121 -68
  68. package/backend/detectors/tier1-lifecycle-hook.js +63 -23
  69. package/backend/detectors/tier1-maintainer-compromise.js +157 -0
  70. package/backend/detectors/tier1-metadata-spoof.js +92 -42
  71. package/backend/detectors/tier1-multistage-postinstall.js +46 -19
  72. package/backend/detectors/tier1-obfuscation-heuristics.js +45 -17
  73. package/backend/detectors/tier1-self-propagation.js +115 -0
  74. package/backend/detectors/tier1-slsa-attestation.js +1 -1
  75. package/backend/detectors/tier1-transitive-deps.js +182 -0
  76. package/backend/detectors/tier1-typosquat.js +129 -50
  77. package/backend/detectors/tier1-version-anomaly.js +77 -41
  78. package/backend/detectors/tier1-version-confusion.js +79 -59
  79. package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
  80. package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
  81. package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
  82. package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
  83. package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
  84. package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
  85. package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
  86. package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
  87. package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
  88. package/backend/detectors/trapdoor/index.js +19 -14
  89. package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
  90. package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
  91. package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
  92. package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
  93. package/backend/detectors.test.js +78 -19
  94. package/backend/fetch.js +37 -29
  95. package/backend/index.js +1 -1
  96. package/backend/license.js +20 -4
  97. package/backend/lockfile.js +60 -36
  98. package/backend/pdf.js +107 -28
  99. package/backend/policy.js +183 -56
  100. package/backend/provenance.js +28 -3
  101. package/backend/report.js +136 -70
  102. package/backend/sbom.js +33 -27
  103. package/backend/scripts/analyze-false-positives.js +14 -8
  104. package/backend/scripts/analyze-validation.js +27 -21
  105. package/backend/scripts/detect-false-positives.js +20 -10
  106. package/backend/scripts/fetch-top-packages.js +197 -49
  107. package/backend/scripts/validate-d10-d13.js +103 -0
  108. package/backend/scripts/validate-detectors.js +26 -17
  109. package/backend/siem/cef.js +23 -21
  110. package/backend/siem/ecs.js +3 -3
  111. package/backend/siem/index.js +1 -1
  112. package/backend/siem/qradar.js +3 -3
  113. package/backend/siem/sentinel.js +2 -2
  114. package/backend/tests-d5-enhanced.test.js +13 -12
  115. package/backend/tests-d6-version-anomaly.test.js +17 -8
  116. package/backend/tests-d6.test.js +24 -14
  117. package/backend/tests-d6c.test.js +27 -14
  118. package/backend/tests-d7-obfuscation.test.js +9 -12
  119. package/backend/tests.test.js +182 -83
  120. package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
  121. package/backend/vsix-scan/detectors/burst-publish.js +14 -7
  122. package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
  123. package/backend/vsix-scan/detectors/known-ioc.js +23 -8
  124. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
  125. package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
  126. package/backend/vsix-scan/index.js +97 -41
  127. package/backend/vsix-scan/marketplace-client.js +29 -13
  128. package/cli/cli.js +154 -64
  129. package/package.json +12 -3
package/backend/policy.js CHANGED
@@ -5,26 +5,89 @@ const SEVERITY_ORDER = ['none', 'low', 'medium', 'high', 'critical'];
5
5
  const VALID_SEVERITIES = new Set(SEVERITY_ORDER);
6
6
 
7
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',
8
+ 'react',
9
+ 'react-dom',
10
+ 'vue',
11
+ 'angular',
12
+ 'next',
13
+ 'nuxt',
14
+ 'express',
15
+ 'fastify',
16
+ 'hono',
17
+ 'koa',
18
+ 'connect',
19
+ 'webpack',
20
+ 'vite',
21
+ 'rollup',
22
+ 'esbuild',
23
+ 'typescript',
24
+ 'babel-core',
25
+ 'lodash',
26
+ 'ramda',
27
+ 'underscore',
28
+ 'axios',
29
+ 'node-fetch',
30
+ 'got',
31
+ 'superagent',
32
+ 'sequelize',
33
+ 'prisma',
34
+ 'typeorm',
35
+ 'mongoose',
36
+ 'jest',
37
+ 'mocha',
38
+ 'vitest',
39
+ 'ava',
40
+ 'prettier',
41
+ 'eslint',
42
+ 'stylelint',
43
+ 'socket.io',
44
+ 'ws',
45
+ 'rimraf',
46
+ 'glob',
47
+ 'minimatch',
48
+ 'fs-extra',
49
+ 'electron',
50
+ 'puppeteer',
51
+ 'playwright',
52
+ 'sharp',
53
+ 'node-canvas',
54
+ 'ffmpeg-static',
55
+ 'turbo',
56
+ 'react-scripts',
57
+ '@angular/cli',
58
+ 'gatsby',
59
+ 'parcel',
60
+ 'tslib',
61
+ 'core-js',
62
+ 'regenerator-runtime',
63
+ 'buffer',
64
+ 'node-gyp',
65
+ 'node-pre-gyp',
66
+ 'winston',
67
+ 'uuid',
68
+ 'moment',
69
+ 'dotenv',
70
+ 'pg',
71
+ 'semver',
72
+ 'redux',
73
+ 'redis',
74
+ 'dayjs',
75
+ 'luxon',
76
+ 'chalk',
77
+ 'debug',
78
+ 'cors',
79
+ 'helmet',
80
+ 'multer',
81
+ 'body-parser',
82
+ 'cheerio',
83
+ 'bluebird',
84
+ 'bcrypt',
85
+ 'commander',
86
+ 'yargs',
87
+ 'passport',
88
+ 'jsonwebtoken',
89
+ 'nodemailer',
90
+ 'class-validator',
28
91
  ]);
29
92
 
30
93
  function severityIndex(s) {
@@ -32,8 +95,12 @@ function severityIndex(s) {
32
95
  }
33
96
 
34
97
  function matchesFilePath(filePath, pattern) {
35
- if (!pattern) return false;
36
- if (pattern === '*') return true;
98
+ if (!pattern) {
99
+ return false;
100
+ }
101
+ if (pattern === '*') {
102
+ return true;
103
+ }
37
104
  const regexPattern = pattern
38
105
  .replace(/\./g, '\\.')
39
106
  .replace(/\*\*/g, '___DOUBLE_STAR___')
@@ -44,49 +111,90 @@ function matchesFilePath(filePath, pattern) {
44
111
 
45
112
  function matchesContext(finding, rule) {
46
113
  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;
114
+ if (!ctx) {
115
+ return false;
116
+ }
117
+
118
+ if (rule.context?.is_dist_build === true && !ctx.is_dist_build) {
119
+ return false;
120
+ }
121
+ if (rule.context?.is_dist_build === false && ctx.is_dist_build) {
122
+ return false;
123
+ }
124
+ if (rule.context?.is_test_fixture === true && !ctx.is_test_fixture) {
125
+ return false;
126
+ }
127
+ if (rule.context?.is_test_fixture === false && ctx.is_test_fixture) {
128
+ return false;
129
+ }
130
+ if (rule.context?.is_lifecycle_hook === true && !ctx.is_lifecycle_hook) {
131
+ return false;
132
+ }
133
+ if (rule.context?.is_lifecycle_hook === false && ctx.is_lifecycle_hook) {
134
+ return false;
135
+ }
136
+ if (rule.context?.is_known_safe_domain === true && !ctx.is_known_safe_domain) {
137
+ return false;
138
+ }
139
+ if (rule.context?.is_known_safe_domain === false && ctx.is_known_safe_domain) {
140
+ return false;
141
+ }
142
+
143
+ if (rule.context?.file_path && !matchesFilePath(ctx.file_path, rule.context.file_path)) {
144
+ return false;
145
+ }
59
146
  if (rule.context?.url_domain) {
60
- if (!ctx.url_domain) return false;
147
+ if (!ctx.url_domain) {
148
+ return false;
149
+ }
61
150
  const domainPattern = rule.context.url_domain.replace(/\*/g, '.*');
62
- if (!new RegExp(`^${domainPattern}$`).test(ctx.url_domain)) return false;
151
+ if (!new RegExp(`^${domainPattern}$`).test(ctx.url_domain)) {
152
+ return false;
153
+ }
63
154
  }
64
155
 
65
156
  return true;
66
157
  }
67
158
 
68
159
  function matchesKnownReputable(packageName) {
69
- if (KNOWN_REPUTABLE_PACKAGES.has(packageName)) return true;
160
+ if (KNOWN_REPUTABLE_PACKAGES.has(packageName)) {
161
+ return true;
162
+ }
70
163
  const [scope, name] = packageName.split('/');
71
- if (scope && name && KNOWN_REPUTABLE_PACKAGES.has(`${scope}/*`)) return true;
164
+ if (scope && name && KNOWN_REPUTABLE_PACKAGES.has(`${scope}/*`)) {
165
+ return true;
166
+ }
72
167
  return false;
73
168
  }
74
169
 
75
170
  function getPackageReputationTier(pkgName) {
76
171
  const name = pkgName?.replace(/^@/, '').replace(/\/.*/, '') || '';
77
- if (matchesKnownReputable(name)) return 'trusted';
172
+ if (matchesKnownReputable(name)) {
173
+ return 'trusted';
174
+ }
78
175
  return 'unknown';
79
176
  }
80
177
 
81
178
  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;
179
+ if (rule.atk_id !== (finding.atk_id || finding.id)) {
180
+ return false;
181
+ }
182
+ if (rule.package && rule.package !== '*' && rule.package !== pkgName) {
183
+ return false;
184
+ }
84
185
 
85
- if (rule.context && !matchesContext(finding, rule)) return false;
186
+ if (rule.context && !matchesContext(finding, rule)) {
187
+ return false;
188
+ }
86
189
 
87
190
  if (rule.reputation_tier) {
88
191
  const tier = getPackageReputationTier(pkgName);
89
- if (rule.reputation_tier !== tier && !(rule.reputation_tier === '*' || rule.reputation_tier === 'any')) return false;
192
+ if (
193
+ rule.reputation_tier !== tier &&
194
+ !(rule.reputation_tier === '*' || rule.reputation_tier === 'any')
195
+ ) {
196
+ return false;
197
+ }
90
198
  }
91
199
 
92
200
  return true;
@@ -109,13 +217,17 @@ function loadPolicy(path) {
109
217
  if (policy.severity_overrides) {
110
218
  for (const [atkId, severity] of Object.entries(policy.severity_overrides)) {
111
219
  if (!VALID_SEVERITIES.has(severity)) {
112
- throw new Error(`Invalid severity "${severity}" for ${atkId} — must be one of: low, medium, high, critical`);
220
+ throw new Error(
221
+ `Invalid severity "${severity}" for ${atkId} — must be one of: low, medium, high, critical`
222
+ );
113
223
  }
114
224
  }
115
225
  }
116
226
 
117
227
  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`);
228
+ throw new Error(
229
+ `Invalid fail_on "${policy.fail_on}" — must be one of: none, low, medium, high, critical`
230
+ );
119
231
  }
120
232
 
121
233
  if (policy.suppress) {
@@ -143,7 +255,7 @@ function sanitizePolicy(policy) {
143
255
  allow: { packages: policy.allow?.packages ?? [] },
144
256
  severity_overrides: policy.severity_overrides ?? {},
145
257
  fail_on: policy.fail_on ?? 'none',
146
- suppress: (policy.suppress ?? []).map(r => ({
258
+ suppress: (policy.suppress ?? []).map((r) => ({
147
259
  atk_id: r.atk_id,
148
260
  package: r.package || '*',
149
261
  reason: r.reason || '',
@@ -154,23 +266,29 @@ function sanitizePolicy(policy) {
154
266
  }
155
267
 
156
268
  function isAllowed(packageName, policy) {
157
- if (!policy.allow.packages.length) return false;
269
+ if (!policy.allow.packages.length) {
270
+ return false;
271
+ }
158
272
  const nameOnly = packageName.split('@')[0];
159
- return policy.allow.packages.some(p => p === packageName || p === nameOnly);
273
+ return policy.allow.packages.some((p) => p === packageName || p === nameOnly);
160
274
  }
161
275
 
162
276
  function applyPolicy(findings, packageName, policy) {
163
277
  let filtered = [...findings];
164
278
 
165
279
  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));
280
+ filtered = filtered.filter((f) => {
281
+ if (f.context?.is_lifecycle_hook) {
282
+ return true;
283
+ }
284
+ if (f.context?.is_multi_layer) {
285
+ return true;
286
+ }
287
+ return !policy.suppress.some((r) => matchesSuppressRule(f, packageName, r));
170
288
  });
171
289
  }
172
290
 
173
- filtered = filtered.map(f => {
291
+ filtered = filtered.map((f) => {
174
292
  const override = policy.severity_overrides[f.atk_id || f.id];
175
293
  if (override) {
176
294
  return { ...f, severity: override, _severityOverridden: true };
@@ -184,10 +302,19 @@ function applyPolicy(findings, packageName, policy) {
184
302
  }
185
303
 
186
304
  function checkFailOn(findings, policy) {
187
- if (policy.fail_on === 'none') return false;
305
+ if (policy.fail_on === 'none') {
306
+ return false;
307
+ }
188
308
 
189
309
  const threshold = severityIndex(policy.fail_on);
190
- return findings.some(f => severityIndex(f.severity) >= threshold);
310
+ return findings.some((f) => severityIndex(f.severity) >= threshold);
191
311
  }
192
312
 
193
- export { loadPolicy, applyPolicy, isAllowed, getPackageReputationTier, matchesContext, KNOWN_REPUTABLE_PACKAGES };
313
+ export {
314
+ loadPolicy,
315
+ applyPolicy,
316
+ isAllowed,
317
+ getPackageReputationTier,
318
+ matchesContext,
319
+ KNOWN_REPUTABLE_PACKAGES,
320
+ };
@@ -12,7 +12,13 @@ export function signManifest(manifest, key = HMAC_KEY) {
12
12
  return createHmac('sha256', key).update(JSON.stringify(manifest)).digest('hex');
13
13
  }
14
14
 
15
- export function buildDetectionRule({ ruleId, ruleName, severity, cveReferences = [], campaignName }) {
15
+ export function buildDetectionRule({
16
+ ruleId,
17
+ ruleName,
18
+ severity,
19
+ cveReferences = [],
20
+ campaignName,
21
+ }) {
16
22
  return {
17
23
  rule_id: ruleId,
18
24
  rule_name: ruleName,
@@ -41,7 +47,12 @@ export function buildDetectionResult({ triggered, severity, indicators = [] }) {
41
47
 
42
48
  export function buildAuditTrail({ detectionLogic, ruleProvenanceUrl, campaignSourceUrl }) {
43
49
  const contentHash = hashContent(detectionLogic);
44
- const manifest = { contentHash, ruleProvenanceUrl, campaignSourceUrl, generatedAt: new Date().toISOString() };
50
+ const manifest = {
51
+ contentHash,
52
+ ruleProvenanceUrl,
53
+ campaignSourceUrl,
54
+ generatedAt: new Date().toISOString(),
55
+ };
45
56
  return {
46
57
  content_hash: contentHash,
47
58
  rule_provenance_url: ruleProvenanceUrl,
@@ -60,7 +71,21 @@ export function buildDetectionRecord({ rule, scanMetadata, detectionResult, audi
60
71
  };
61
72
  }
62
73
 
63
- export function attachProvenance(evidence, { ruleId, ruleName, severity, campaignName, pkgName, pkgVersion, triggered, indicators, ruleProvenanceUrl, campaignSourceUrl }) {
74
+ export function attachProvenance(
75
+ evidence,
76
+ {
77
+ ruleId,
78
+ ruleName,
79
+ severity,
80
+ campaignName,
81
+ pkgName,
82
+ pkgVersion,
83
+ triggered,
84
+ indicators,
85
+ ruleProvenanceUrl,
86
+ campaignSourceUrl,
87
+ }
88
+ ) {
64
89
  const rule = buildDetectionRule({ ruleId, ruleName, severity, campaignName });
65
90
  const scanMetadata = buildScanMetadata({
66
91
  scannerVersion: '@lateos/npm-scan',
package/backend/report.js CHANGED
@@ -1,21 +1,29 @@
1
1
  export function generateHTML(scans) {
2
- const rows = scans.map(s => {
2
+ const rows = scans.map((s) => {
3
3
  const findings = s.findings || [];
4
4
  const sevMap = { critical: 5, high: 4, medium: 3, low: 2, info: 1 };
5
5
  const worst = findings.reduce((m, f) => Math.max(m, sevMap[f.severity] || 0), 0);
6
6
  const worstLabel = ['', 'info', 'low', 'medium', 'high', 'critical'][worst] || 'clean';
7
- const color = { critical: '#d73a49', high: '#cb2431', medium: '#f66a0a', low: '#dbab09', clean: '#28a745' }[worstLabel] || '#28a745';
8
- const findingRows = findings.map(f =>
9
- `<tr><td>${f.atk_id || f.id}</td><td style="color:${color}">${f.severity}</td><td>${f.description || f.title || ''}</td><td>${(f.evidence || '').slice(0, 80)}</td></tr>`
10
- ).join('');
7
+ const color =
8
+ { critical: '#d73a49', high: '#cb2431', medium: '#f66a0a', low: '#dbab09', clean: '#28a745' }[
9
+ worstLabel
10
+ ] || '#28a745';
11
+ const findingRows = findings
12
+ .map(
13
+ (f) =>
14
+ `<tr><td>${f.atk_id || f.id}</td><td style="color:${color}">${f.severity}</td><td>${f.description || f.title || ''}</td><td>${(f.evidence || '').slice(0, 80)}</td></tr>`
15
+ )
16
+ .join('');
11
17
  return { name: s.package_name, worstLabel, color, count: findings.length, findingRows };
12
18
  });
13
19
 
14
- const criticalCount = scans.filter(s => s.findings?.some(f => f.severity === 'critical')).length;
15
- const highCount = scans.filter(s => s.findings?.some(f => f.severity === 'high')).length;
16
- const mediumCount = scans.filter(s => s.findings?.some(f => f.severity === 'medium')).length;
17
- const lowCount = scans.filter(s => s.findings?.some(f => f.severity === 'low')).length;
18
- const cleanCount = scans.filter(s => !s.findings?.length).length;
20
+ const criticalCount = scans.filter((s) =>
21
+ s.findings?.some((f) => f.severity === 'critical')
22
+ ).length;
23
+ const highCount = scans.filter((s) => s.findings?.some((f) => f.severity === 'high')).length;
24
+ const mediumCount = scans.filter((s) => s.findings?.some((f) => f.severity === 'medium')).length;
25
+ const lowCount = scans.filter((s) => s.findings?.some((f) => f.severity === 'low')).length;
26
+ const cleanCount = scans.filter((s) => !s.findings?.length).length;
19
27
 
20
28
  const nistMap = generateNistTable(scans);
21
29
 
@@ -59,7 +67,7 @@ th { background: #161b22; font-weight: 600; }
59
67
  <h2>Findings</h2>
60
68
  <table>
61
69
  <thead><tr><th>ATK</th><th>Severity</th><th>Title</th><th>Evidence</th></tr></thead>
62
- <tbody>${rows.map(r => `<tr><td colspan="4" style="background:#161b22;font-weight:600">${r.name} <span class="badge ${r.worstLabel}">${r.count ? r.worstLabel : 'clean'}</span></td></tr>${r.findingRows}`).join('')}</tbody>
70
+ <tbody>${rows.map((r) => `<tr><td colspan="4" style="background:#161b22;font-weight:600">${r.name} <span class="badge ${r.worstLabel}">${r.count ? r.worstLabel : 'clean'}</span></td></tr>${r.findingRows}`).join('')}</tbody>
63
71
  </table>
64
72
 
65
73
  <h2>NIST SP 800-161 Compliance Summary</h2>
@@ -73,9 +81,11 @@ ${nistMap}
73
81
  function getAtkFindings(scans) {
74
82
  const map = {};
75
83
  for (const s of scans) {
76
- for (const f of (s.findings || [])) {
84
+ for (const f of s.findings || []) {
77
85
  const key = f.atk_id || f.id;
78
- if (!map[key]) map[key] = [];
86
+ if (!map[key]) {
87
+ map[key] = [];
88
+ }
79
89
  map[key].push(f);
80
90
  }
81
91
  }
@@ -117,7 +127,9 @@ export function generateText(scans) {
117
127
  const worst = findings.reduce((m, f) => Math.max(m, sevMap[f.severity] || 0), 0);
118
128
  const worstLabel = sevLabel[worst] || 'clean';
119
129
 
120
- lines.push(`${s.package_name}@${s.version || 'unknown'} \u2500\u2500 ${findings.length} findings (worst: ${worstLabel})`);
130
+ lines.push(
131
+ `${s.package_name}@${s.version || 'unknown'} \u2500\u2500 ${findings.length} findings (worst: ${worstLabel})`
132
+ );
121
133
 
122
134
  for (const f of findings) {
123
135
  const desc = (f.description || f.title || '').slice(0, 80);
@@ -159,41 +171,46 @@ function generateNistTable(scans) {
159
171
 
160
172
  export function generateSARIF(scan, format = 'json') {
161
173
  const findings = scan.findings || [];
162
- const runs = [{
163
- tool: {
164
- driver: {
165
- name: 'npm-scan',
166
- version: '0.9.7',
167
- informationUri: 'https://github.com/lateos-ai/npm-scan',
168
- rules: Array.from(new Set(findings.map(f => f.id))).map(id => ({
169
- id,
170
- name: `ATK-${id.replace('ATK-', '')}`,
171
- shortDescription: { text: findings.find(f => f.id === id)?.title || id },
172
- fullDescription: { text: findings.find(f => f.id === id)?.description || '' },
173
- defaultConfiguration: { enabled: true }
174
- }))
175
- }
174
+ const runs = [
175
+ {
176
+ tool: {
177
+ driver: {
178
+ name: 'npm-scan',
179
+ version: '0.9.7',
180
+ informationUri: 'https://github.com/lateos-ai/npm-scan',
181
+ rules: Array.from(new Set(findings.map((f) => f.id))).map((id) => ({
182
+ id,
183
+ name: `ATK-${id.replace('ATK-', '')}`,
184
+ shortDescription: { text: findings.find((f) => f.id === id)?.title || id },
185
+ fullDescription: { text: findings.find((f) => f.id === id)?.description || '' },
186
+ defaultConfiguration: { enabled: true },
187
+ })),
188
+ },
189
+ },
190
+ results: findings.map((f) => {
191
+ const severityMap = { critical: 'error', high: 'error', medium: 'warning', low: 'note' };
192
+ return {
193
+ ruleId: f.id,
194
+ level: severityMap[f.severity] || 'note',
195
+ message: { text: f.description || f.title },
196
+ locations: [
197
+ {
198
+ physicalLocation: {
199
+ artifactLocation: { uri: f.evidence || 'unknown' },
200
+ region: { startLine: 1, startColumn: 1 },
201
+ },
202
+ },
203
+ ],
204
+ };
205
+ }),
176
206
  },
177
- results: findings.map(f => {
178
- const severityMap = { critical: 'error', high: 'error', medium: 'warning', low: 'note' };
179
- return {
180
- ruleId: f.id,
181
- level: severityMap[f.severity] || 'note',
182
- message: { text: f.description || f.title },
183
- locations: [{
184
- physicalLocation: {
185
- artifactLocation: { uri: f.evidence || 'unknown' },
186
- region: { startLine: 1, startColumn: 1 }
187
- }
188
- }]
189
- };
190
- })
191
- }];
207
+ ];
192
208
 
193
209
  const sarif = {
194
210
  version: '2.1.0',
195
- schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
196
- runs
211
+ schema:
212
+ 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
213
+ runs,
197
214
  };
198
215
 
199
216
  return format === 'pretty' ? JSON.stringify(sarif, null, 2) : JSON.stringify(sarif);
@@ -201,17 +218,21 @@ export function generateSARIF(scan, format = 'json') {
201
218
 
202
219
  export function generateCSV(scans) {
203
220
  const headers = 'id,severity,title,description,evidence,package_name,version\n';
204
- const rows = (scans || []).flatMap(s =>
205
- (s.findings || []).map(f => [
206
- f.id,
207
- f.severity || '',
208
- (f.title || '').replace(/,/g, ';'),
209
- (f.description || '').replace(/,/g, ';'),
210
- (f.evidence || '').replace(/,/g, ';'),
211
- s.package_name || '',
212
- s.version || ''
213
- ].join(','))
214
- ).join('\n');
221
+ const rows = (scans || [])
222
+ .flatMap((s) =>
223
+ (s.findings || []).map((f) =>
224
+ [
225
+ f.id,
226
+ f.severity || '',
227
+ (f.title || '').replace(/,/g, ';'),
228
+ (f.description || '').replace(/,/g, ';'),
229
+ (f.evidence || '').replace(/,/g, ';'),
230
+ s.package_name || '',
231
+ s.version || '',
232
+ ].join(',')
233
+ )
234
+ )
235
+ .join('\n');
215
236
  return headers + rows;
216
237
  }
217
238
 
@@ -222,25 +243,70 @@ export function calculateRiskScore(findings, totalPackages = 1) {
222
243
  }
223
244
 
224
245
  const STIG_MAP = {
225
- 'SRG-APP-000141': { title: 'Application Malware Detection', atk: 'ATK-001', desc: 'Lifecycle script detection' },
226
- 'SRG-APP-000142': { title: 'Application Code Obfuscation', atk: 'ATK-002', desc: 'Obfuscated payload detection' },
227
- 'SRG-APP-000143': { title: 'Credential Harvesting', atk: 'ATK-003', desc: 'Credential exfiltration detection' },
228
- 'SRG-APP-000144': { title: 'Persistence Mechanisms', atk: 'ATK-004', desc: 'Malicious persistence detection' },
229
- 'SRG-APP-000145': { title: 'Data Exfiltration', atk: 'ATK-005', desc: 'Network exfiltration detection' },
230
- 'SRG-APP-000146': { title: 'Dependency Confusion', atk: 'ATK-006', desc: 'Internal package detection' },
231
- 'SRG-APP-000147': { title: 'Typosquatting', atk: 'ATK-007', desc: 'Malicious package name detection' },
232
- 'SRG-APP-000148': { title: 'Tarball Tampering', atk: 'ATK-008', desc: 'Modified package detection' },
233
- 'SRG-APP-000149': { title: 'Dormant Triggers', atk: 'ATK-009', desc: 'Conditional execution detection' },
234
- 'SRG-APP-000150': { title: 'Sandbox Evasion', atk: 'ATK-010', desc: 'Environment detection evasion' },
235
- 'SRG-APP-000151': { title: 'Transitive Propagation', atk: 'ATK-011', desc: 'Dependency chain attacks' }
246
+ 'SRG-APP-000141': {
247
+ title: 'Application Malware Detection',
248
+ atk: 'ATK-001',
249
+ desc: 'Lifecycle script detection',
250
+ },
251
+ 'SRG-APP-000142': {
252
+ title: 'Application Code Obfuscation',
253
+ atk: 'ATK-002',
254
+ desc: 'Obfuscated payload detection',
255
+ },
256
+ 'SRG-APP-000143': {
257
+ title: 'Credential Harvesting',
258
+ atk: 'ATK-003',
259
+ desc: 'Credential exfiltration detection',
260
+ },
261
+ 'SRG-APP-000144': {
262
+ title: 'Persistence Mechanisms',
263
+ atk: 'ATK-004',
264
+ desc: 'Malicious persistence detection',
265
+ },
266
+ 'SRG-APP-000145': {
267
+ title: 'Data Exfiltration',
268
+ atk: 'ATK-005',
269
+ desc: 'Network exfiltration detection',
270
+ },
271
+ 'SRG-APP-000146': {
272
+ title: 'Dependency Confusion',
273
+ atk: 'ATK-006',
274
+ desc: 'Internal package detection',
275
+ },
276
+ 'SRG-APP-000147': {
277
+ title: 'Typosquatting',
278
+ atk: 'ATK-007',
279
+ desc: 'Malicious package name detection',
280
+ },
281
+ 'SRG-APP-000148': {
282
+ title: 'Tarball Tampering',
283
+ atk: 'ATK-008',
284
+ desc: 'Modified package detection',
285
+ },
286
+ 'SRG-APP-000149': {
287
+ title: 'Dormant Triggers',
288
+ atk: 'ATK-009',
289
+ desc: 'Conditional execution detection',
290
+ },
291
+ 'SRG-APP-000150': {
292
+ title: 'Sandbox Evasion',
293
+ atk: 'ATK-010',
294
+ desc: 'Environment detection evasion',
295
+ },
296
+ 'SRG-APP-000151': {
297
+ title: 'Transitive Propagation',
298
+ atk: 'ATK-011',
299
+ desc: 'Dependency chain attacks',
300
+ },
236
301
  };
237
302
 
238
303
  export function generateSTIG(scans) {
239
304
  const rows = [];
240
305
  for (const [stigId, info] of Object.entries(STIG_MAP)) {
241
- const findings = scans.flatMap(s => (s.findings || []).filter(f => f.id === info.atk));
306
+ const findings = scans.flatMap((s) => (s.findings || []).filter((f) => f.id === info.atk));
242
307
  const status = findings.length > 0 ? 'NOT APPLICABLE' : 'COMPLETE';
243
- const findingsList = findings.map(f => `${f.severity.toUpperCase()}: ${f.title}`).join('; ') || 'None';
308
+ const findingsList =
309
+ findings.map((f) => `${f.severity.toUpperCase()}: ${f.title}`).join('; ') || 'None';
244
310
  rows.push(`| ${stigId} | ${info.title} | ${status} | ${findingsList} |`);
245
311
  }
246
312
  return `# STIG Compliance Report
@@ -252,4 +318,4 @@ ${rows.join('\n')}
252
318
 
253
319
  ---
254
320
  *This report maps application security controls to DISA STIG requirements.*`;
255
- }
321
+ }