fad-checker 1.0.3 → 1.0.4
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/data/cwe-names.json +192 -0
- package/lib/cve-report.js +329 -20
- package/lib/nvd.js +7 -1
- package/package.json +4 -4
- package/test/cve-report.test.js +92 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Common CWE identifiers → short human name. Covers the bulk of CVEs in practice. Source: cwe.mitre.org. Names are abbreviated for compact UI display.",
|
|
3
|
+
"CWE-20": "Improper Input Validation",
|
|
4
|
+
"CWE-22": "Path Traversal",
|
|
5
|
+
"CWE-59": "Link Following",
|
|
6
|
+
"CWE-74": "Improper Neutralization of Special Elements (Injection)",
|
|
7
|
+
"CWE-77": "Command Injection",
|
|
8
|
+
"CWE-78": "OS Command Injection",
|
|
9
|
+
"CWE-79": "Cross-site Scripting (XSS)",
|
|
10
|
+
"CWE-88": "Argument Injection",
|
|
11
|
+
"CWE-89": "SQL Injection",
|
|
12
|
+
"CWE-90": "LDAP Injection",
|
|
13
|
+
"CWE-91": "XML Injection",
|
|
14
|
+
"CWE-93": "CRLF Injection",
|
|
15
|
+
"CWE-94": "Code Injection",
|
|
16
|
+
"CWE-95": "Eval Injection",
|
|
17
|
+
"CWE-113": "HTTP Response Splitting",
|
|
18
|
+
"CWE-115": "Misinterpretation of Input",
|
|
19
|
+
"CWE-116": "Improper Encoding or Escaping of Output",
|
|
20
|
+
"CWE-117": "Improper Output Neutralization for Logs",
|
|
21
|
+
"CWE-118": "Incorrect Access of Indexable Resource",
|
|
22
|
+
"CWE-119": "Improper Restriction of Operations within the Bounds of a Memory Buffer",
|
|
23
|
+
"CWE-120": "Buffer Copy without Checking Size (Classic Buffer Overflow)",
|
|
24
|
+
"CWE-121": "Stack-based Buffer Overflow",
|
|
25
|
+
"CWE-122": "Heap-based Buffer Overflow",
|
|
26
|
+
"CWE-125": "Out-of-bounds Read",
|
|
27
|
+
"CWE-129": "Improper Validation of Array Index",
|
|
28
|
+
"CWE-131": "Incorrect Calculation of Buffer Size",
|
|
29
|
+
"CWE-134": "Use of Externally-Controlled Format String",
|
|
30
|
+
"CWE-178": "Improper Handling of Case Sensitivity",
|
|
31
|
+
"CWE-184": "Incomplete List of Disallowed Inputs",
|
|
32
|
+
"CWE-185": "Incorrect Regular Expression",
|
|
33
|
+
"CWE-190": "Integer Overflow or Wraparound",
|
|
34
|
+
"CWE-191": "Integer Underflow",
|
|
35
|
+
"CWE-193": "Off-by-one Error",
|
|
36
|
+
"CWE-200": "Exposure of Sensitive Information to an Unauthorized Actor",
|
|
37
|
+
"CWE-203": "Observable Discrepancy",
|
|
38
|
+
"CWE-204": "Observable Response Discrepancy",
|
|
39
|
+
"CWE-208": "Observable Timing Discrepancy",
|
|
40
|
+
"CWE-209": "Generation of Error Message Containing Sensitive Information",
|
|
41
|
+
"CWE-212": "Improper Removal of Sensitive Information Before Storage or Transfer",
|
|
42
|
+
"CWE-215": "Insertion of Sensitive Information Into Debugging Code",
|
|
43
|
+
"CWE-226": "Sensitive Information in Resource Not Removed Before Reuse",
|
|
44
|
+
"CWE-241": "Improper Handling of Unexpected Data Type",
|
|
45
|
+
"CWE-248": "Uncaught Exception",
|
|
46
|
+
"CWE-252": "Unchecked Return Value",
|
|
47
|
+
"CWE-256": "Plaintext Storage of a Password",
|
|
48
|
+
"CWE-257": "Storing Passwords in a Recoverable Format",
|
|
49
|
+
"CWE-259": "Use of Hard-coded Password",
|
|
50
|
+
"CWE-260": "Password in Configuration File",
|
|
51
|
+
"CWE-261": "Weak Encoding for Password",
|
|
52
|
+
"CWE-262": "Not Using Password Aging",
|
|
53
|
+
"CWE-269": "Improper Privilege Management",
|
|
54
|
+
"CWE-272": "Least Privilege Violation",
|
|
55
|
+
"CWE-273": "Improper Check for Dropped Privileges",
|
|
56
|
+
"CWE-275": "Permission Issues",
|
|
57
|
+
"CWE-276": "Incorrect Default Permissions",
|
|
58
|
+
"CWE-281": "Improper Preservation of Permissions",
|
|
59
|
+
"CWE-284": "Improper Access Control",
|
|
60
|
+
"CWE-285": "Improper Authorization",
|
|
61
|
+
"CWE-287": "Improper Authentication",
|
|
62
|
+
"CWE-288": "Authentication Bypass Using an Alternate Path or Channel",
|
|
63
|
+
"CWE-290": "Authentication Bypass by Spoofing",
|
|
64
|
+
"CWE-294": "Authentication Bypass by Capture-replay",
|
|
65
|
+
"CWE-295": "Improper Certificate Validation",
|
|
66
|
+
"CWE-296": "Improper Following of a Certificate's Chain of Trust",
|
|
67
|
+
"CWE-297": "Improper Validation of Certificate with Host Mismatch",
|
|
68
|
+
"CWE-298": "Improper Validation of Certificate Expiration",
|
|
69
|
+
"CWE-299": "Improper Check for Certificate Revocation",
|
|
70
|
+
"CWE-300": "Channel Accessible by Non-Endpoint (MITM)",
|
|
71
|
+
"CWE-303": "Incorrect Implementation of Authentication Algorithm",
|
|
72
|
+
"CWE-305": "Authentication Bypass by Primary Weakness",
|
|
73
|
+
"CWE-306": "Missing Authentication for Critical Function",
|
|
74
|
+
"CWE-307": "Improper Restriction of Excessive Authentication Attempts",
|
|
75
|
+
"CWE-311": "Missing Encryption of Sensitive Data",
|
|
76
|
+
"CWE-312": "Cleartext Storage of Sensitive Information",
|
|
77
|
+
"CWE-319": "Cleartext Transmission of Sensitive Information",
|
|
78
|
+
"CWE-320": "Key Management Errors",
|
|
79
|
+
"CWE-321": "Use of Hard-coded Cryptographic Key",
|
|
80
|
+
"CWE-326": "Inadequate Encryption Strength",
|
|
81
|
+
"CWE-327": "Use of a Broken or Risky Cryptographic Algorithm",
|
|
82
|
+
"CWE-328": "Use of Weak Hash",
|
|
83
|
+
"CWE-329": "Generation of Predictable IV with CBC Mode",
|
|
84
|
+
"CWE-330": "Use of Insufficiently Random Values",
|
|
85
|
+
"CWE-331": "Insufficient Entropy",
|
|
86
|
+
"CWE-335": "Incorrect Usage of Seeds in PRNG",
|
|
87
|
+
"CWE-338": "Use of Cryptographically Weak PRNG",
|
|
88
|
+
"CWE-345": "Insufficient Verification of Data Authenticity",
|
|
89
|
+
"CWE-346": "Origin Validation Error",
|
|
90
|
+
"CWE-347": "Improper Verification of Cryptographic Signature",
|
|
91
|
+
"CWE-352": "Cross-Site Request Forgery (CSRF)",
|
|
92
|
+
"CWE-354": "Improper Validation of Integrity Check Value",
|
|
93
|
+
"CWE-358": "Improperly Implemented Security Check for Standard",
|
|
94
|
+
"CWE-362": "Race Condition",
|
|
95
|
+
"CWE-367": "TOCTOU Race Condition",
|
|
96
|
+
"CWE-369": "Divide By Zero",
|
|
97
|
+
"CWE-377": "Insecure Temporary File",
|
|
98
|
+
"CWE-378": "Creation of Temporary File With Insecure Permissions",
|
|
99
|
+
"CWE-384": "Session Fixation",
|
|
100
|
+
"CWE-390": "Detection of Error Condition Without Action",
|
|
101
|
+
"CWE-400": "Uncontrolled Resource Consumption (DoS)",
|
|
102
|
+
"CWE-401": "Missing Release of Memory after Effective Lifetime",
|
|
103
|
+
"CWE-402": "Transmission of Private Resources into a New Sphere",
|
|
104
|
+
"CWE-404": "Improper Resource Shutdown or Release",
|
|
105
|
+
"CWE-405": "Asymmetric Resource Consumption (Amplification)",
|
|
106
|
+
"CWE-407": "Inefficient Algorithmic Complexity",
|
|
107
|
+
"CWE-409": "Improper Handling of Highly Compressed Data (Zip Bomb)",
|
|
108
|
+
"CWE-415": "Double Free",
|
|
109
|
+
"CWE-416": "Use After Free",
|
|
110
|
+
"CWE-419": "Unprotected Primary Channel",
|
|
111
|
+
"CWE-425": "Direct Request (Forced Browsing)",
|
|
112
|
+
"CWE-426": "Untrusted Search Path",
|
|
113
|
+
"CWE-427": "Uncontrolled Search Path Element",
|
|
114
|
+
"CWE-428": "Unquoted Search Path or Element",
|
|
115
|
+
"CWE-434": "Unrestricted Upload of File with Dangerous Type",
|
|
116
|
+
"CWE-436": "Interpretation Conflict",
|
|
117
|
+
"CWE-441": "Unintended Proxy or Intermediary (Confused Deputy)",
|
|
118
|
+
"CWE-444": "HTTP Request/Response Smuggling",
|
|
119
|
+
"CWE-451": "UI Misrepresentation of Critical Information",
|
|
120
|
+
"CWE-459": "Incomplete Cleanup",
|
|
121
|
+
"CWE-470": "Unsafe Reflection",
|
|
122
|
+
"CWE-471": "Modification of Assumed-Immutable Data",
|
|
123
|
+
"CWE-476": "NULL Pointer Dereference",
|
|
124
|
+
"CWE-477": "Use of Obsolete Function",
|
|
125
|
+
"CWE-489": "Active Debug Code",
|
|
126
|
+
"CWE-494": "Download of Code Without Integrity Check",
|
|
127
|
+
"CWE-497": "Exposure of Sensitive System Information to an Unauthorized Control Sphere",
|
|
128
|
+
"CWE-502": "Deserialization of Untrusted Data",
|
|
129
|
+
"CWE-521": "Weak Password Requirements",
|
|
130
|
+
"CWE-522": "Insufficiently Protected Credentials",
|
|
131
|
+
"CWE-523": "Unprotected Transport of Credentials",
|
|
132
|
+
"CWE-532": "Insertion of Sensitive Information into Log File",
|
|
133
|
+
"CWE-540": "Inclusion of Sensitive Information in Source Code",
|
|
134
|
+
"CWE-548": "Exposure of Information Through Directory Listing",
|
|
135
|
+
"CWE-552": "Files or Directories Accessible to External Parties",
|
|
136
|
+
"CWE-565": "Reliance on Cookies without Validation and Integrity Checking",
|
|
137
|
+
"CWE-601": "URL Redirection to Untrusted Site (Open Redirect)",
|
|
138
|
+
"CWE-610": "Externally Controlled Reference to a Resource in Another Sphere",
|
|
139
|
+
"CWE-611": "XML External Entity (XXE)",
|
|
140
|
+
"CWE-613": "Insufficient Session Expiration",
|
|
141
|
+
"CWE-617": "Reachable Assertion",
|
|
142
|
+
"CWE-639": "Authorization Bypass Through User-Controlled Key (IDOR)",
|
|
143
|
+
"CWE-640": "Weak Password Recovery Mechanism for Forgotten Password",
|
|
144
|
+
"CWE-641": "Improper Restriction of Names for Files and Other Resources",
|
|
145
|
+
"CWE-643": "XPath Injection",
|
|
146
|
+
"CWE-645": "Overly Restrictive Account Lockout Mechanism",
|
|
147
|
+
"CWE-652": "XQuery Injection",
|
|
148
|
+
"CWE-665": "Improper Initialization",
|
|
149
|
+
"CWE-668": "Exposure of Resource to Wrong Sphere",
|
|
150
|
+
"CWE-670": "Always-Incorrect Control Flow Implementation",
|
|
151
|
+
"CWE-672": "Operation on a Resource after Expiration or Release",
|
|
152
|
+
"CWE-674": "Uncontrolled Recursion",
|
|
153
|
+
"CWE-682": "Incorrect Calculation",
|
|
154
|
+
"CWE-693": "Protection Mechanism Failure",
|
|
155
|
+
"CWE-697": "Incorrect Comparison",
|
|
156
|
+
"CWE-704": "Incorrect Type Conversion or Cast",
|
|
157
|
+
"CWE-732": "Incorrect Permission Assignment for Critical Resource",
|
|
158
|
+
"CWE-749": "Exposed Dangerous Method or Function",
|
|
159
|
+
"CWE-754": "Improper Check for Unusual or Exceptional Conditions",
|
|
160
|
+
"CWE-755": "Improper Handling of Exceptional Conditions",
|
|
161
|
+
"CWE-763": "Release of Invalid Pointer or Reference",
|
|
162
|
+
"CWE-770": "Allocation of Resources Without Limits or Throttling",
|
|
163
|
+
"CWE-772": "Missing Release of Resource after Effective Lifetime",
|
|
164
|
+
"CWE-776": "XML Entity Expansion (XEE / Billion Laughs)",
|
|
165
|
+
"CWE-787": "Out-of-bounds Write",
|
|
166
|
+
"CWE-789": "Memory Allocation with Excessive Size Value",
|
|
167
|
+
"CWE-798": "Use of Hard-coded Credentials",
|
|
168
|
+
"CWE-829": "Inclusion of Functionality from Untrusted Control Sphere",
|
|
169
|
+
"CWE-834": "Excessive Iteration",
|
|
170
|
+
"CWE-835": "Loop with Unreachable Exit Condition (Infinite Loop)",
|
|
171
|
+
"CWE-838": "Inappropriate Encoding for Output Context",
|
|
172
|
+
"CWE-843": "Type Confusion (Access of Resource Using Incompatible Type)",
|
|
173
|
+
"CWE-862": "Missing Authorization",
|
|
174
|
+
"CWE-863": "Incorrect Authorization",
|
|
175
|
+
"CWE-908": "Use of Uninitialized Resource",
|
|
176
|
+
"CWE-913": "Improper Control of Dynamically-Managed Code Resources",
|
|
177
|
+
"CWE-915": "Mass Assignment",
|
|
178
|
+
"CWE-916": "Use of Password Hash With Insufficient Computational Effort",
|
|
179
|
+
"CWE-917": "Expression Language Injection (EL Injection)",
|
|
180
|
+
"CWE-918": "Server-Side Request Forgery (SSRF)",
|
|
181
|
+
"CWE-920": "Improper Restriction of Power Consumption",
|
|
182
|
+
"CWE-921": "Storage of Sensitive Data in a Mechanism without Access Control",
|
|
183
|
+
"CWE-924": "Improper Enforcement of Message Integrity During Transmission in a Communication Channel",
|
|
184
|
+
"CWE-1004": "Sensitive Cookie Without 'HttpOnly' Flag",
|
|
185
|
+
"CWE-1021": "Improper Restriction of Rendered UI Layers (Clickjacking)",
|
|
186
|
+
"CWE-1188": "Insecure Default Initialization of Resource",
|
|
187
|
+
"CWE-1236": "CSV Formula Injection",
|
|
188
|
+
"CWE-1275": "Sensitive Cookie with Improper SameSite Attribute",
|
|
189
|
+
"CWE-1284": "Improper Validation of Specified Quantity in Input",
|
|
190
|
+
"CWE-1321": "Improperly Controlled Modification of Object Prototype (Prototype Pollution)",
|
|
191
|
+
"CWE-1333": "Inefficient Regular Expression Complexity (ReDoS)"
|
|
192
|
+
}
|
package/lib/cve-report.js
CHANGED
|
@@ -8,6 +8,21 @@
|
|
|
8
8
|
const fs = require("fs");
|
|
9
9
|
const path = require("path");
|
|
10
10
|
|
|
11
|
+
// CWE-id → short human name. Loaded once at module init. Unknown CWEs fall
|
|
12
|
+
// back to the raw id (with a "—" placeholder name where the row asks for one).
|
|
13
|
+
const CWE_NAMES = (() => {
|
|
14
|
+
try {
|
|
15
|
+
const raw = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "data", "cwe-names.json"), "utf8"));
|
|
16
|
+
delete raw._comment;
|
|
17
|
+
return raw;
|
|
18
|
+
} catch { return {}; }
|
|
19
|
+
})();
|
|
20
|
+
function cweName(id) {
|
|
21
|
+
if (!id) return "";
|
|
22
|
+
const key = String(id).toUpperCase();
|
|
23
|
+
return CWE_NAMES[key] || "";
|
|
24
|
+
}
|
|
25
|
+
|
|
11
26
|
function esc(s) {
|
|
12
27
|
if (s == null) return "";
|
|
13
28
|
return String(s)
|
|
@@ -84,6 +99,17 @@ td.cwe .cwe-link { color: #6366f1; text-decoration: none; }
|
|
|
84
99
|
td.cwe .cwe-link:hover { text-decoration: underline; }
|
|
85
100
|
td.cwe .cwe-empty { color: #d1d5db; }
|
|
86
101
|
td.cwe .cwe-more { color: #9ca3af; font-style: italic; font-size: 10px; }
|
|
102
|
+
td.cwe .cwe-name { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 10px; color: #6b7280; line-height: 1.3; margin-top: 1px; max-width: 220px; }
|
|
103
|
+
.cwe-list { list-style: none; padding: 0; margin: 0; }
|
|
104
|
+
.cwe-list li { padding: 3px 0; font-size: 12px; line-height: 1.5; }
|
|
105
|
+
.cwe-list li .cwe-link { font-family: ui-monospace, monospace; color: #4338ca; text-decoration: none; font-weight: 600; }
|
|
106
|
+
.cwe-list li .cwe-link:hover { text-decoration: underline; }
|
|
107
|
+
.cwe-list li .cwe-name-inline { color: #374151; }
|
|
108
|
+
.cwe-list li .cwe-empty { color: #9ca3af; font-style: italic; }
|
|
109
|
+
/* CWE chip in the exec summary */
|
|
110
|
+
.exec-summary .exec-cwe { display: inline-flex; align-items: baseline; gap: 4px; font-size: 11px; color: #4b5563; background: #eef2ff; border: 1px solid #c7d2fe; padding: 1px 6px; border-radius: 3px; margin-left: 2px; }
|
|
111
|
+
.exec-summary .exec-cwe .exec-cwe-id { font-family: ui-monospace, monospace; font-weight: 600; color: #4338ca; }
|
|
112
|
+
.exec-summary .exec-cwe .exec-cwe-name { color: #4b5563; }
|
|
87
113
|
td.published { font-family: ui-monospace, monospace; font-size: 11px; color: #6b7280; white-space: nowrap; }
|
|
88
114
|
tr.cve-row.cpe-fp td { opacity: .55; }
|
|
89
115
|
.jump-badge { display: inline-block; padding: 1px 8px; border-radius: 4px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; }
|
|
@@ -196,6 +222,14 @@ details.report-subsection details.report-subsection details.report-subsection >
|
|
|
196
222
|
.toolbar .btn.primary { background: #4f46e5; color: #fff; border-color: #4f46e5; }
|
|
197
223
|
.toolbar .btn.primary:hover { background: #4338ca; }
|
|
198
224
|
.toolbar .toolbar-label { font-size: 11px; color: #6b7280; }
|
|
225
|
+
/* Table copy controls — one toolbar per table */
|
|
226
|
+
.table-wrap { margin: 8px 0 24px; }
|
|
227
|
+
.table-wrap > table { margin: 0; }
|
|
228
|
+
.table-actions { display: flex; justify-content: flex-end; gap: 6px; margin-bottom: 4px; }
|
|
229
|
+
.btn-copy { display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; font-size: 11px; background: #fff; border: 1px solid #d1d5db; border-radius: 4px; cursor: pointer; color: #4b5563; font-family: inherit; transition: all .15s ease; }
|
|
230
|
+
.btn-copy:hover { background: #eef2ff; border-color: #6366f1; color: #4338ca; }
|
|
231
|
+
.btn-copy.flashing-ok { background: #10b981; color: #fff; border-color: #10b981; }
|
|
232
|
+
.btn-copy.flashing-err { background: #dc2626; color: #fff; border-color: #dc2626; }
|
|
199
233
|
.fix-recipe { background: #fff7ed; border: 1px solid #fed7aa; border-left: 4px solid #f97316; padding: 10px 14px; border-radius: 4px; margin: 6px 0; font-size: 12px; }
|
|
200
234
|
.fix-recipe code { display: block; background: #1f2937; color: #f3f4f6; padding: 8px 10px; border-radius: 3px; font-size: 11px; margin-top: 6px; white-space: pre; overflow-x: auto; }
|
|
201
235
|
.fix-recipe .recipe-title { font-weight: 600; margin-bottom: 2px; }
|
|
@@ -424,7 +458,12 @@ function renderDetailPanel(m) {
|
|
|
424
458
|
sections.push(section("Description", `<div class="full-desc">${esc(cve.description)}</div>`));
|
|
425
459
|
}
|
|
426
460
|
|
|
427
|
-
// 2.
|
|
461
|
+
// 2. Weaknesses (CWE) — id + human name, links to mitre.org
|
|
462
|
+
if (Array.isArray(cve.cwes) && cve.cwes.length) {
|
|
463
|
+
sections.push(section("Weaknesses (CWE)", formatCwesVerbose(cve.cwes), { count: cve.cwes.length }));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// 3. Metadata
|
|
428
467
|
const kv = [];
|
|
429
468
|
if (cve.cvssVector) kv.push(`<div><label>${esc(cve.cvssVersion || "CVSS")}</label><span class="cvss-vector">${esc(cve.cvssVector)}</span></div>`);
|
|
430
469
|
if (cve.score != null) kv.push(`<div><label>Base score</label><span>${esc(cve.score.toFixed ? cve.score.toFixed(1) : cve.score)} (${esc(cve.severity || "?")})</span></div>`);
|
|
@@ -502,7 +541,29 @@ function formatCwes(cwes) {
|
|
|
502
541
|
if (!Array.isArray(cwes) || !cwes.length) return `<span class="cwe-empty">—</span>`;
|
|
503
542
|
const top = cwes.slice(0, 2);
|
|
504
543
|
const more = cwes.length > 2 ? ` <span class="cwe-more">+${cwes.length - 2}</span>` : "";
|
|
505
|
-
return top.map(c =>
|
|
544
|
+
return top.map(c => {
|
|
545
|
+
const id = String(c);
|
|
546
|
+
const name = cweName(id);
|
|
547
|
+
const num = id.replace(/^CWE-/i, "");
|
|
548
|
+
const link = `<a href="https://cwe.mitre.org/data/definitions/${esc(num)}.html" target="_blank" rel="noopener" class="cwe-link"${name ? ` title="${esc(id)}: ${esc(name)}"` : ""}>${esc(id)}</a>`;
|
|
549
|
+
// Show the human name inline beneath the id (compact, italic) when we know it
|
|
550
|
+
return name ? `${link}<div class="cwe-name">${esc(name)}</div>` : link;
|
|
551
|
+
}).join(" ") + more;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Verbose CWE list for the detail panel: id link + full human name on the
|
|
555
|
+
// same line, no truncation. Used in renderDetailPanel.
|
|
556
|
+
function formatCwesVerbose(cwes) {
|
|
557
|
+
if (!Array.isArray(cwes) || !cwes.length) return "";
|
|
558
|
+
return `<ul class="cwe-list">${cwes.map(c => {
|
|
559
|
+
const id = String(c);
|
|
560
|
+
const name = cweName(id);
|
|
561
|
+
const num = id.replace(/^CWE-/i, "");
|
|
562
|
+
return `<li>
|
|
563
|
+
<a href="https://cwe.mitre.org/data/definitions/${esc(num)}.html" target="_blank" rel="noopener" class="cwe-link">${esc(id)}</a>
|
|
564
|
+
${name ? `<span class="cwe-name-inline">— ${esc(name)}</span>` : `<span class="cwe-empty">(unknown weakness)</span>`}
|
|
565
|
+
</li>`;
|
|
566
|
+
}).join("")}</ul>`;
|
|
506
567
|
}
|
|
507
568
|
|
|
508
569
|
function renderCveRow(m, opts = {}) {
|
|
@@ -1125,13 +1186,22 @@ function renderExecutiveSummary({ projectInfo, prodStats, devStats, retireStats,
|
|
|
1125
1186
|
|
|
1126
1187
|
const topPreview = (topCriticalMatches || []).length ? `<div class="exec-top">
|
|
1127
1188
|
<div class="exec-top-title">Top ${topCriticalMatches.length} most critical (preview)</div>
|
|
1128
|
-
<ul class="exec-top-list">${topCriticalMatches.map(m =>
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1189
|
+
<ul class="exec-top-list">${topCriticalMatches.map(m => {
|
|
1190
|
+
// First known-CWE on the match gets surfaced in the summary so the
|
|
1191
|
+
// reader has a one-line answer to "what kind of weakness is this?"
|
|
1192
|
+
const cwes = Array.isArray(m.cve.cwes) ? m.cve.cwes : [];
|
|
1193
|
+
const primary = cwes.find(c => cweName(c)) || cwes[0];
|
|
1194
|
+
const primaryName = primary ? cweName(primary) : "";
|
|
1195
|
+
const cweChip = primary ? `<span class="exec-cwe" title="${esc(primary)}${primaryName ? ": " + esc(primaryName) : ""}"><span class="exec-cwe-id">${esc(primary)}</span>${primaryName ? `<span class="exec-cwe-name">${esc(primaryName)}</span>` : ""}</span>` : "";
|
|
1196
|
+
return `<li>
|
|
1197
|
+
${badge((m.cve.severity || "HIGH").toUpperCase())}
|
|
1198
|
+
<a class="exec-cve-link" href="#ch${m.dep?.scope === "vendored" ? "2" : "1"}">${esc(m.cve.id)}</a>
|
|
1199
|
+
<code>${esc(depDisplayName(m.dep))}</code>
|
|
1200
|
+
<span class="exec-ver">${esc(m.dep.version || "?")}</span>
|
|
1201
|
+
${m.cve.fixVersion ? `<span class="exec-fix">→ ${esc(m.cve.fixVersion)}</span>` : ""}
|
|
1202
|
+
${cweChip}
|
|
1203
|
+
</li>`;
|
|
1204
|
+
}).join("")}</ul>
|
|
1135
1205
|
</div>` : "";
|
|
1136
1206
|
|
|
1137
1207
|
return `<div class="exec-summary exec-${esc(level.toLowerCase())}">
|
|
@@ -1186,7 +1256,7 @@ function renderWarningItems(items, itemLimit) {
|
|
|
1186
1256
|
return `<ul class="warn-items">${lis}${overflow}</ul>`;
|
|
1187
1257
|
}
|
|
1188
1258
|
|
|
1189
|
-
function buildBody({ cveMatches, devCveMatches, retireMatches, eolResults, obsoleteResults, outdatedResults, resolvedDeps, projectInfo, warnings }) {
|
|
1259
|
+
function buildBody({ cveMatches, devCveMatches, retireMatches, eolResults, obsoleteResults, outdatedResults, resolvedDeps, projectInfo, warnings, wrapTables = true }) {
|
|
1190
1260
|
setRenderCtx({ srcRoot: projectInfo?.src || null });
|
|
1191
1261
|
// Dedupe rows so a single (dep, cve) shows once with all via paths merged.
|
|
1192
1262
|
cveMatches = mergeMatches(cveMatches || []);
|
|
@@ -1244,7 +1314,7 @@ function buildBody({ cveMatches, devCveMatches, retireMatches, eolResults, obsol
|
|
|
1244
1314
|
fpTotal: prodMatchesFP.length + devMatchesFP.length,
|
|
1245
1315
|
});
|
|
1246
1316
|
|
|
1247
|
-
|
|
1317
|
+
const body = `
|
|
1248
1318
|
<header class="report-header">
|
|
1249
1319
|
<h1>FAD-Checker Report</h1>
|
|
1250
1320
|
<div class="report-subtitle">Multi-ecosystem dependency security audit</div>
|
|
@@ -1268,6 +1338,7 @@ function buildBody({ cveMatches, devCveMatches, retireMatches, eolResults, obsol
|
|
|
1268
1338
|
${majorSection(`7. Fix Recommendations`, renderFixRecommendations(recos, prodMatchesActive, outdatedResults), { open: false, id: "ch7" })}
|
|
1269
1339
|
${(prodMatchesFP.length + devMatchesFP.length) ? majorSection(`8. Appendix: Likely false positives (CPE-filtered) (${prodMatchesFP.length + devMatchesFP.length})`, fpContent, { open: false, id: "ch8" }) : ""}
|
|
1270
1340
|
`;
|
|
1341
|
+
return wrapTables ? wrapTablesWithCopyButtons(body) : body;
|
|
1271
1342
|
}
|
|
1272
1343
|
|
|
1273
1344
|
/**
|
|
@@ -1346,6 +1417,127 @@ function renderCveBySectionByEco(matches, chapterPrefix, srcRoot) {
|
|
|
1346
1417
|
return out.join("") || `<div class="empty">No CVE matches.</div>`;
|
|
1347
1418
|
}
|
|
1348
1419
|
|
|
1420
|
+
/**
|
|
1421
|
+
* Wrap every <table>…</table> in the body HTML with a `.table-wrap` div that
|
|
1422
|
+
* carries a small toolbar with a "Copy table" button. The button is wired up
|
|
1423
|
+
* by COPY_SCRIPT below — it copies the table as styled HTML (Word-compatible)
|
|
1424
|
+
* with a TSV fallback for text-only paste targets.
|
|
1425
|
+
*
|
|
1426
|
+
* Skipped for the Word output (wrapTables=false in buildBody) since Word can't
|
|
1427
|
+
* run JS and the bare table is what we want there.
|
|
1428
|
+
*/
|
|
1429
|
+
function wrapTablesWithCopyButtons(html) {
|
|
1430
|
+
return html.replace(/(<table\b[^>]*>[\s\S]*?<\/table>)/g, (match) => {
|
|
1431
|
+
return `<div class="table-wrap"><div class="table-actions"><button class="btn-copy" type="button" title="Copy this table — paste it into Word with formatting preserved">📋 Copy table</button></div>${match}</div>`;
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
const COPY_SCRIPT = `
|
|
1436
|
+
<script>
|
|
1437
|
+
(function(){
|
|
1438
|
+
// Inline styles applied to clipboard-bound table so it survives pasting into
|
|
1439
|
+
// Word with reasonable formatting (Word ignores most external CSS classes,
|
|
1440
|
+
// but honours inline style="…" attributes).
|
|
1441
|
+
function inlineStylesForWord(table){
|
|
1442
|
+
var clone = table.cloneNode(true);
|
|
1443
|
+
// Drop expandable detail rows (they would paste as huge empty cells)
|
|
1444
|
+
clone.querySelectorAll('tr.detail-row').forEach(function(r){ r.remove(); });
|
|
1445
|
+
// Strip our interactive classes so Word doesn't try to interpret them
|
|
1446
|
+
clone.querySelectorAll('tr.cve-row').forEach(function(r){
|
|
1447
|
+
r.classList.remove('cve-row');
|
|
1448
|
+
r.classList.remove('open');
|
|
1449
|
+
r.classList.remove('cpe-fp');
|
|
1450
|
+
});
|
|
1451
|
+
clone.setAttribute('border', '1');
|
|
1452
|
+
clone.setAttribute('cellspacing', '0');
|
|
1453
|
+
clone.setAttribute('cellpadding', '6');
|
|
1454
|
+
clone.setAttribute('width', '100%');
|
|
1455
|
+
clone.setAttribute('style', 'width:100%; border-collapse:collapse; font-family:Calibri,Arial,sans-serif; font-size:10pt; color:#1f2937;');
|
|
1456
|
+
clone.querySelectorAll('th').forEach(function(th){
|
|
1457
|
+
th.setAttribute('style', 'background:#f3f4f6; padding:6px 8px; border:1px solid #d1d5db; text-align:left; font-weight:bold; font-size:9pt; text-transform:uppercase; letter-spacing:.5px; color:#4b5563;');
|
|
1458
|
+
});
|
|
1459
|
+
clone.querySelectorAll('td').forEach(function(td){
|
|
1460
|
+
var prev = td.getAttribute('style') || '';
|
|
1461
|
+
td.setAttribute('style', 'padding:6px 8px; border:1px solid #e5e7eb; vertical-align:top; ' + prev);
|
|
1462
|
+
});
|
|
1463
|
+
// Replace severity badges with plain coloured text so they survive paste
|
|
1464
|
+
clone.querySelectorAll('span[style*="border-radius"]').forEach(function(s){
|
|
1465
|
+
var m = (s.getAttribute('style') || '').match(/background:\\s*(#[0-9a-fA-F]{3,6})/);
|
|
1466
|
+
var bg = m ? m[1] : '#666';
|
|
1467
|
+
s.setAttribute('style', 'background:' + bg + '; color:#fff; padding:2px 6px; font-weight:bold; font-size:9pt;');
|
|
1468
|
+
});
|
|
1469
|
+
return clone;
|
|
1470
|
+
}
|
|
1471
|
+
function wordWrapper(tableHtml){
|
|
1472
|
+
return '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40"><head><meta charset="utf-8"></head><body>' + tableHtml + '</body></html>';
|
|
1473
|
+
}
|
|
1474
|
+
function tableToTsv(table){
|
|
1475
|
+
var rows = Array.prototype.filter.call(table.querySelectorAll('tr'), function(r){ return !r.classList.contains('detail-row'); });
|
|
1476
|
+
return rows.map(function(r){
|
|
1477
|
+
return Array.prototype.map.call(r.querySelectorAll('th, td'), function(c){
|
|
1478
|
+
return (c.innerText || c.textContent || '').replace(/[\\t\\r\\n]+/g, ' ').trim();
|
|
1479
|
+
}).join('\\t');
|
|
1480
|
+
}).join('\\n');
|
|
1481
|
+
}
|
|
1482
|
+
function flash(btn, cls, msg){
|
|
1483
|
+
var old = btn.textContent;
|
|
1484
|
+
btn.textContent = msg;
|
|
1485
|
+
btn.classList.add(cls);
|
|
1486
|
+
setTimeout(function(){
|
|
1487
|
+
btn.textContent = old;
|
|
1488
|
+
btn.classList.remove(cls);
|
|
1489
|
+
}, 1500);
|
|
1490
|
+
}
|
|
1491
|
+
async function copyTable(table, btn){
|
|
1492
|
+
var clone = inlineStylesForWord(table);
|
|
1493
|
+
var html = wordWrapper(clone.outerHTML);
|
|
1494
|
+
var tsv = tableToTsv(table);
|
|
1495
|
+
// Modern path: ClipboardItem with text/html + text/plain
|
|
1496
|
+
try {
|
|
1497
|
+
if (window.ClipboardItem && navigator.clipboard && navigator.clipboard.write) {
|
|
1498
|
+
await navigator.clipboard.write([
|
|
1499
|
+
new ClipboardItem({
|
|
1500
|
+
'text/html': new Blob([html], { type: 'text/html' }),
|
|
1501
|
+
'text/plain': new Blob([tsv], { type: 'text/plain' })
|
|
1502
|
+
})
|
|
1503
|
+
]);
|
|
1504
|
+
flash(btn, 'flashing-ok', '✓ Copied!');
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
} catch(e) { /* fall through to legacy path */ }
|
|
1508
|
+
// Legacy path: select clone, execCommand('copy') — gets the HTML
|
|
1509
|
+
try {
|
|
1510
|
+
var temp = document.createElement('div');
|
|
1511
|
+
temp.setAttribute('contenteditable', 'true');
|
|
1512
|
+
temp.style.position = 'fixed';
|
|
1513
|
+
temp.style.left = '-9999px';
|
|
1514
|
+
temp.style.top = '0';
|
|
1515
|
+
temp.innerHTML = clone.outerHTML;
|
|
1516
|
+
document.body.appendChild(temp);
|
|
1517
|
+
var range = document.createRange();
|
|
1518
|
+
range.selectNodeContents(temp);
|
|
1519
|
+
var sel = window.getSelection();
|
|
1520
|
+
sel.removeAllRanges();
|
|
1521
|
+
sel.addRange(range);
|
|
1522
|
+
var ok = document.execCommand('copy');
|
|
1523
|
+
sel.removeAllRanges();
|
|
1524
|
+
document.body.removeChild(temp);
|
|
1525
|
+
flash(btn, ok ? 'flashing-ok' : 'flashing-err', ok ? '✓ Copied!' : '✗ Copy failed');
|
|
1526
|
+
} catch(e) {
|
|
1527
|
+
flash(btn, 'flashing-err', '✗ Copy failed');
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
document.querySelectorAll('.btn-copy').forEach(function(btn){
|
|
1531
|
+
btn.addEventListener('click', function(e){
|
|
1532
|
+
e.stopPropagation();
|
|
1533
|
+
var wrap = btn.closest('.table-wrap');
|
|
1534
|
+
var table = wrap && wrap.querySelector('table');
|
|
1535
|
+
if (table) copyTable(table, btn);
|
|
1536
|
+
});
|
|
1537
|
+
});
|
|
1538
|
+
})();
|
|
1539
|
+
</script>`;
|
|
1540
|
+
|
|
1349
1541
|
const TOGGLE_SCRIPT = `
|
|
1350
1542
|
<script>
|
|
1351
1543
|
(function(){
|
|
@@ -1402,13 +1594,128 @@ const TOGGLE_SCRIPT = `
|
|
|
1402
1594
|
function generateHtmlReport(payload) {
|
|
1403
1595
|
return `<!doctype html>
|
|
1404
1596
|
<html><head><meta charset="utf-8"><title>FAD-Checker Report</title><style>${CSS}</style></head>
|
|
1405
|
-
<body>${buildBody(payload)}${TOGGLE_SCRIPT}</body></html>`;
|
|
1597
|
+
<body>${buildBody(payload)}${TOGGLE_SCRIPT}${COPY_SCRIPT}</body></html>`;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// Word-specific CSS tweaks layered on top of the shared CSS:
|
|
1601
|
+
// - landscape A4 with tight margins to maximise usable column width
|
|
1602
|
+
// - thead repeats on every page break, rows stay together
|
|
1603
|
+
// - sticky/transitions/box-shadow stripped (Word renders them oddly)
|
|
1604
|
+
// - chevron pseudo-elements hidden (Word can't toggle them anyway)
|
|
1605
|
+
// - tables get an explicit 100% width attribute so Word fits-to-page
|
|
1606
|
+
// - smaller font / padding inside tables to cram more columns into the page
|
|
1607
|
+
//
|
|
1608
|
+
// Dimensions are given in cm (A4 landscape = 29.7×21cm). The `mso-page-orientation`
|
|
1609
|
+
// property is the actually-honoured-by-Word landscape switch; without it Word
|
|
1610
|
+
// often defaults to portrait even when @page asks for landscape.
|
|
1611
|
+
const WORD_CSS = `
|
|
1612
|
+
@page WordSection1 {
|
|
1613
|
+
size: 29.7cm 21cm;
|
|
1614
|
+
mso-page-orientation: landscape;
|
|
1615
|
+
margin: 1cm 1cm 1cm 1cm;
|
|
1616
|
+
mso-header-margin: .5cm;
|
|
1617
|
+
mso-footer-margin: .5cm;
|
|
1618
|
+
mso-paper-source: 0;
|
|
1619
|
+
}
|
|
1620
|
+
div.WordSection1 { page: WordSection1; }
|
|
1621
|
+
body { margin: 0; background: #fff; }
|
|
1622
|
+
/* Tables: hard-fit to page width. table-layout:fixed forces Word to honour
|
|
1623
|
+
width:100% even when content would naturally be wider. */
|
|
1624
|
+
table { mso-table-lspace: 0; mso-table-rspace: 0; width: 100% !important; max-width: 100% !important; table-layout: fixed; font-size: 9pt; }
|
|
1625
|
+
th, td { padding: 4px 6px !important; font-size: 9pt; word-break: break-word; overflow-wrap: anywhere; }
|
|
1626
|
+
th { font-size: 8pt; }
|
|
1627
|
+
tr { page-break-inside: avoid; }
|
|
1628
|
+
thead { display: table-header-group; }
|
|
1629
|
+
h1, h2, h3, summary { page-break-after: avoid; }
|
|
1630
|
+
details.report-section, details.report-subsection { page-break-inside: auto; }
|
|
1631
|
+
/* Strip interactive affordances Word can't honour */
|
|
1632
|
+
.toolbar, .toc { display: none !important; }
|
|
1633
|
+
details > summary { cursor: default; list-style: none; }
|
|
1634
|
+
details.report-section > summary h2::before,
|
|
1635
|
+
details.report-subsection > summary h3::before { content: "" !important; display: none !important; }
|
|
1636
|
+
tr.cve-row td:first-child::before { content: "" !important; display: none !important; }
|
|
1637
|
+
tr.cve-row, tr.cve-row.open { background: #fff !important; cursor: default; }
|
|
1638
|
+
/* Detail rows always shown (no JS toggling in Word) */
|
|
1639
|
+
tr.detail-row { display: table-row !important; }
|
|
1640
|
+
/* Remove shadows / sticky / hover effects that confuse Word */
|
|
1641
|
+
.report-header, .exec-summary, .summary .card, .warnings { box-shadow: none !important; }
|
|
1642
|
+
.fix-recipe code { white-space: pre-wrap; word-break: break-word; overflow-x: visible; font-size: 8pt; }
|
|
1643
|
+
/* Long URLs and coordinates: wrap instead of overflow */
|
|
1644
|
+
td.cve, td.dep, .via, .ref-list li, .defined-in code { word-break: break-word; overflow-wrap: anywhere; }
|
|
1645
|
+
/* Tighter colour palette so Word prints cleanly */
|
|
1646
|
+
tr:nth-child(even) { background: #fafafa; }
|
|
1647
|
+
/* Detail panel: shrink so expanded CVE blocks don't dominate the page */
|
|
1648
|
+
.detail-panel { padding: 8px 12px; font-size: 9pt; }
|
|
1649
|
+
.detail-panel .full-desc { font-size: 9pt; line-height: 1.4; }
|
|
1650
|
+
`;
|
|
1651
|
+
|
|
1652
|
+
// Section properties Word reads from a conditional comment. This is the
|
|
1653
|
+
// actually-reliable way to force landscape — @page rules in <style> are
|
|
1654
|
+
// inconsistently honoured across Word versions, but `<w:sectPr>` is.
|
|
1655
|
+
// Page size in twips: A4 landscape = 16838 × 11906 (1cm ≈ 567 twips).
|
|
1656
|
+
const WORD_SECTION_PR = `<!--[if gte mso 9]>
|
|
1657
|
+
<xml>
|
|
1658
|
+
<w:WordDocument>
|
|
1659
|
+
<w:View>Print</w:View>
|
|
1660
|
+
<w:Zoom>100</w:Zoom>
|
|
1661
|
+
<w:DoNotOptimizeForBrowser/>
|
|
1662
|
+
</w:WordDocument>
|
|
1663
|
+
</xml>
|
|
1664
|
+
<![endif]-->
|
|
1665
|
+
<!--[if gte mso 9]>
|
|
1666
|
+
<style>
|
|
1667
|
+
@page WordSection1 {
|
|
1668
|
+
size: 16838twips 11906twips;
|
|
1669
|
+
mso-page-orientation: landscape;
|
|
1670
|
+
margin: 567twips 567twips 567twips 567twips;
|
|
1671
|
+
}
|
|
1672
|
+
</style>
|
|
1673
|
+
<![endif]-->`;
|
|
1674
|
+
|
|
1675
|
+
// Column widths (in %) per known table shape. table-layout:fixed needs these
|
|
1676
|
+
// so Word can lay out the columns deterministically instead of inflating wide
|
|
1677
|
+
// columns based on natural content width.
|
|
1678
|
+
const TABLE_COLGROUPS = {
|
|
1679
|
+
// 8-col CVE table (header starts with <th>Severity</th><th>CVE ID</th>…)
|
|
1680
|
+
cve: { match: /<thead><tr><th>Severity<\/th><th>CVE ID<\/th><th>Dependency<\/th><th>CWE<\/th><th>Published<\/th><th>Description<\/th><th>Fix Version<\/th><th>Source<\/th>(<th>Recommended action<\/th>)?<\/tr><\/thead>/,
|
|
1681
|
+
widths8: [7, 10, 15, 10, 7, 26, 11, 14],
|
|
1682
|
+
widths9: [6, 9, 13, 9, 6, 22, 10, 13, 12] },
|
|
1683
|
+
// 5-col EOL table
|
|
1684
|
+
eol: { match: /<thead><tr><th>Product<\/th><th>Dependency<\/th><th>EOL date<\/th><th>Latest<\/th><th>Notes<\/th><\/tr><\/thead>/,
|
|
1685
|
+
widths: [15, 25, 12, 12, 36] },
|
|
1686
|
+
// 4-col Obsolete table
|
|
1687
|
+
obsolete: { match: /<thead><tr><th>Severity<\/th><th>Obsolete<\/th><th>Replacement<\/th><th>Why<\/th><\/tr><\/thead>/,
|
|
1688
|
+
widths: [10, 25, 25, 40] },
|
|
1689
|
+
// 5-col Outdated table
|
|
1690
|
+
outdated: { match: /<thead><tr><th>Jump<\/th><th>Dependency<\/th><th>Current<\/th><th>Latest<\/th><th>Released<\/th><\/tr><\/thead>/,
|
|
1691
|
+
widths: [12, 38, 18, 18, 14] },
|
|
1692
|
+
// 6-col Retire.js table
|
|
1693
|
+
retire: { match: /<thead><tr><th>Sev<\/th><th>Library<\/th><th>Vendored file<\/th><th>CVE \/ GHSA<\/th><th>Fixed in<\/th><th>Summary<\/th><\/tr><\/thead>/,
|
|
1694
|
+
widths: [8, 18, 24, 14, 12, 24] },
|
|
1695
|
+
};
|
|
1696
|
+
|
|
1697
|
+
function colgroupHtml(widths) {
|
|
1698
|
+
return `<colgroup>${widths.map(w => `<col style="width:${w}%">`).join("")}</colgroup>`;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// Inject a <colgroup> right after each table opening tag so Word respects the
|
|
1702
|
+
// width split. Detects which schema the table follows by its <thead>.
|
|
1703
|
+
function injectColgroups(html) {
|
|
1704
|
+
// CVE 8-col / 9-col (action variant)
|
|
1705
|
+
html = html.replace(new RegExp(`(<table[^>]*>)\\s*(${TABLE_COLGROUPS.cve.match.source})`, "g"), (m, tableTag, thead) => {
|
|
1706
|
+
const widths = thead.includes("Recommended action") ? TABLE_COLGROUPS.cve.widths9 : TABLE_COLGROUPS.cve.widths8;
|
|
1707
|
+
return `${tableTag}${colgroupHtml(widths)}${thead}`;
|
|
1708
|
+
});
|
|
1709
|
+
for (const key of ["eol", "obsolete", "outdated", "retire"]) {
|
|
1710
|
+
const def = TABLE_COLGROUPS[key];
|
|
1711
|
+
html = html.replace(new RegExp(`(<table[^>]*>)\\s*(${def.match.source})`, "g"),
|
|
1712
|
+
(m, tableTag, thead) => `${tableTag}${colgroupHtml(def.widths)}${thead}`);
|
|
1713
|
+
}
|
|
1714
|
+
return html;
|
|
1406
1715
|
}
|
|
1407
1716
|
|
|
1408
|
-
// Word can't run scripts and doesn't honour display:none reliably enough — so
|
|
1409
|
-
// the .doc variant ships with every section/detail/row already expanded.
|
|
1410
1717
|
function generateWordReport(payload) {
|
|
1411
|
-
let body = buildBody(payload);
|
|
1718
|
+
let body = buildBody({ ...payload, wrapTables: false });
|
|
1412
1719
|
// Force-open every <details> and every cve-row/detail-row in the static markup
|
|
1413
1720
|
body = body.replace(/<details ([^>]*?)>/g, (m, attrs) => {
|
|
1414
1721
|
if (attrs.includes(" open")) return m;
|
|
@@ -1418,6 +1725,10 @@ function generateWordReport(payload) {
|
|
|
1418
1725
|
body = body.replace(/<tr class="cve-row">/g, '<tr class="cve-row open">');
|
|
1419
1726
|
// Drop the interactive toolbar (no buttons useful in a print/Word view)
|
|
1420
1727
|
body = body.replace(/<div class="toolbar">[\s\S]*?<\/div>/, "");
|
|
1728
|
+
// Annotate every <table> with explicit width="100%" so Word resizes to page width
|
|
1729
|
+
body = body.replace(/<table>/g, '<table border="1" cellspacing="0" cellpadding="4" width="100%" style="width:100%;border-collapse:collapse;table-layout:fixed">');
|
|
1730
|
+
// Insert <colgroup> with %-widths so Word can split fixed-layout columns
|
|
1731
|
+
body = injectColgroups(body);
|
|
1421
1732
|
return `<html xmlns:o="urn:schemas-microsoft-com:office:office"
|
|
1422
1733
|
xmlns:w="urn:schemas-microsoft-com:office:word"
|
|
1423
1734
|
xmlns="http://www.w3.org/TR/REC-html40">
|
|
@@ -1427,12 +1738,10 @@ function generateWordReport(payload) {
|
|
|
1427
1738
|
<meta name="ProgId" content="Word.Document">
|
|
1428
1739
|
<meta name="Generator" content="Microsoft Word 15">
|
|
1429
1740
|
<title>FAD-Checker Report</title>
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
tr.detail-row { display: table-row !important; }
|
|
1433
|
-
</style>
|
|
1741
|
+
${WORD_SECTION_PR}
|
|
1742
|
+
<style>${CSS}${WORD_CSS}</style>
|
|
1434
1743
|
</head>
|
|
1435
|
-
<body>${body}</body>
|
|
1744
|
+
<body><div class="WordSection1">${body}</div></body>
|
|
1436
1745
|
</html>`;
|
|
1437
1746
|
}
|
|
1438
1747
|
|
package/lib/nvd.js
CHANGED
|
@@ -18,6 +18,10 @@ const { getNvdApiKey } = require("./config");
|
|
|
18
18
|
|
|
19
19
|
const NVD_CACHE_DIR = path.join(os.homedir(), ".fad-checker", "nvd-cache");
|
|
20
20
|
const NVD_CACHE_TTL_MS = 7 * 24 * 3600 * 1000;
|
|
21
|
+
// Bump whenever `extractFromNvdRecord` adds a new field — pre-bump cache
|
|
22
|
+
// entries are treated as cache-miss so we re-fetch and pick up the new field.
|
|
23
|
+
// 2: added `cwes` (Common Weakness Enumeration list).
|
|
24
|
+
const NVD_CACHE_SCHEMA = 2;
|
|
21
25
|
const NVD_BASE = "https://services.nvd.nist.gov/rest/json/cves/2.0";
|
|
22
26
|
|
|
23
27
|
function getRateDelay() {
|
|
@@ -43,6 +47,8 @@ function readCache(cveId) {
|
|
|
43
47
|
if (!fs.existsSync(p)) return null;
|
|
44
48
|
try {
|
|
45
49
|
const data = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
50
|
+
// Schema mismatch → force re-fetch so newly-added fields (e.g. cwes) get populated.
|
|
51
|
+
if ((data._schema || 1) < NVD_CACHE_SCHEMA) return null;
|
|
46
52
|
if (Date.now() - data._fetchedAt < NVD_CACHE_TTL_MS) return data.body;
|
|
47
53
|
} catch { /* ignore */ }
|
|
48
54
|
return null;
|
|
@@ -50,7 +56,7 @@ function readCache(cveId) {
|
|
|
50
56
|
|
|
51
57
|
function writeCache(cveId, body) {
|
|
52
58
|
fs.mkdirSync(NVD_CACHE_DIR, { recursive: true });
|
|
53
|
-
fs.writeFileSync(cachePath(cveId), JSON.stringify({ _fetchedAt: Date.now(), body }));
|
|
59
|
+
fs.writeFileSync(cachePath(cveId), JSON.stringify({ _schema: NVD_CACHE_SCHEMA, _fetchedAt: Date.now(), body }));
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
function bestMetric(metrics) {
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fad-checker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Fucking Autonomous Dependency Checker — multi-ecosystem CVE / EOL / outdated / vendored-JS scanner for Maven, npm and Yarn monorepos. Self-contained HTML + Word report.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"author": "
|
|
6
|
+
"author": "pp9Ping <pp9Ping@gmail.com>",
|
|
7
7
|
"maintainers": [
|
|
8
|
-
"
|
|
8
|
+
"pp9Ping <pp9Ping@gmail.com>"
|
|
9
9
|
],
|
|
10
10
|
"contributors": [
|
|
11
|
-
"
|
|
11
|
+
"pp9Ping <pp9Ping@gmail.com>"
|
|
12
12
|
],
|
|
13
13
|
"bin": {
|
|
14
14
|
"fad-checker": "fad-checker.js",
|
package/test/cve-report.test.js
CHANGED
|
@@ -60,6 +60,98 @@ test("generateWordReport contains Word XML namespaces", () => {
|
|
|
60
60
|
assert.ok(doc.includes("Word.Document"));
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
test("generateHtmlReport wraps tables with copy buttons", () => {
|
|
64
|
+
const html = generateHtmlReport({
|
|
65
|
+
cveMatches: sampleMatches,
|
|
66
|
+
eolResults: [], obsoleteResults: [], outdatedResults: [],
|
|
67
|
+
projectInfo,
|
|
68
|
+
});
|
|
69
|
+
// Wrapper div + copy button injected around every table
|
|
70
|
+
assert.ok(html.includes('class="table-wrap"'), "should wrap tables");
|
|
71
|
+
assert.ok(html.includes('class="btn-copy"'), "should have copy buttons");
|
|
72
|
+
assert.ok(html.includes("📋 Copy table"), "button label");
|
|
73
|
+
// Copy script wired up
|
|
74
|
+
assert.ok(html.includes("navigator.clipboard"), "should include clipboard script");
|
|
75
|
+
assert.ok(html.includes("inlineStylesForWord"), "should inline styles for Word paste");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("generateWordReport does NOT wrap tables (no copy buttons in .doc)", () => {
|
|
79
|
+
const doc = generateWordReport({
|
|
80
|
+
cveMatches: sampleMatches,
|
|
81
|
+
eolResults: [], obsoleteResults: [], outdatedResults: [],
|
|
82
|
+
projectInfo,
|
|
83
|
+
});
|
|
84
|
+
assert.ok(!doc.includes('class="table-wrap"'), "no wrapper in Word output");
|
|
85
|
+
assert.ok(!doc.includes('class="btn-copy"'), "no copy buttons in Word output");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("CWE human names are shown in the CVE table and detail panel", () => {
|
|
89
|
+
const html = generateHtmlReport({
|
|
90
|
+
cveMatches: sampleMatches.map(m => ({
|
|
91
|
+
...m,
|
|
92
|
+
cve: { ...m.cve, cwes: ["CWE-502", "CWE-917"] },
|
|
93
|
+
})),
|
|
94
|
+
eolResults: [], obsoleteResults: [], outdatedResults: [],
|
|
95
|
+
projectInfo,
|
|
96
|
+
});
|
|
97
|
+
// Detail panel: full name inline
|
|
98
|
+
assert.ok(html.includes("Deserialization of Untrusted Data"), "CWE-502 name in panel");
|
|
99
|
+
assert.ok(html.includes("Expression Language Injection"), "CWE-917 name in panel");
|
|
100
|
+
// Tooltip on the column link
|
|
101
|
+
assert.ok(html.includes('title="CWE-502: Deserialization of Untrusted Data"'), "tooltip");
|
|
102
|
+
// Weaknesses section in detail panel
|
|
103
|
+
assert.ok(html.includes("Weaknesses (CWE)"), "weaknesses section heading");
|
|
104
|
+
assert.ok(html.includes('class="cwe-list"'), "CWE list rendered");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("Executive summary surfaces primary CWE name on each preview row", () => {
|
|
108
|
+
const html = generateHtmlReport({
|
|
109
|
+
cveMatches: [{
|
|
110
|
+
dep: { groupId: "org.apache.logging.log4j", artifactId: "log4j-core", version: "2.14.0", scope: "compile" },
|
|
111
|
+
cve: { id: "CVE-2021-44228", severity: "CRITICAL", score: 10, description: "Log4Shell", fixVersion: "2.15.0", cwes: ["CWE-502", "CWE-917"] },
|
|
112
|
+
confidence: "exact",
|
|
113
|
+
}],
|
|
114
|
+
eolResults: [], obsoleteResults: [], outdatedResults: [],
|
|
115
|
+
projectInfo,
|
|
116
|
+
});
|
|
117
|
+
assert.ok(html.includes('class="exec-cwe"'), "exec-cwe chip in summary");
|
|
118
|
+
assert.ok(html.includes('class="exec-cwe-id">CWE-502'), "id in chip");
|
|
119
|
+
assert.ok(html.includes("Deserialization of Untrusted Data"), "human name in chip");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("Unknown CWE falls back to id-only display", () => {
|
|
123
|
+
const html = generateHtmlReport({
|
|
124
|
+
cveMatches: [{
|
|
125
|
+
dep: { groupId: "x", artifactId: "y", version: "1", scope: "compile" },
|
|
126
|
+
cve: { id: "CVE-2099-9999", severity: "HIGH", description: "n/a", cwes: ["CWE-99999"] },
|
|
127
|
+
}],
|
|
128
|
+
eolResults: [], obsoleteResults: [], outdatedResults: [],
|
|
129
|
+
projectInfo,
|
|
130
|
+
});
|
|
131
|
+
assert.ok(html.includes("CWE-99999"), "unknown CWE id present");
|
|
132
|
+
assert.ok(html.includes("(unknown weakness)"), "fallback label for unknown CWE in detail panel");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("generateWordReport applies width:100% to every table for Word page-fit", () => {
|
|
136
|
+
const doc = generateWordReport({
|
|
137
|
+
cveMatches: sampleMatches,
|
|
138
|
+
eolResults: [], obsoleteResults: [], outdatedResults: [],
|
|
139
|
+
projectInfo,
|
|
140
|
+
});
|
|
141
|
+
// Every <table> rewritten with width="100%" + inline style (incl. fixed table-layout)
|
|
142
|
+
assert.ok(doc.includes('width="100%"'), "tables get width=100% attribute");
|
|
143
|
+
assert.ok(doc.includes("table-layout:fixed"), "tables get fixed layout");
|
|
144
|
+
// Word section + landscape page setup
|
|
145
|
+
assert.ok(doc.includes('class="WordSection1"'), "WordSection1 wrapper");
|
|
146
|
+
assert.ok(doc.includes("mso-page-orientation: landscape"), "landscape declared via mso-* property");
|
|
147
|
+
assert.ok(doc.includes("29.7cm 21cm"), "explicit A4 landscape dimensions");
|
|
148
|
+
// MSO conditional comment block with twips-based <w:sectPr> equivalent
|
|
149
|
+
assert.ok(doc.includes("w:WordDocument"), "Word document XML block");
|
|
150
|
+
assert.ok(doc.includes("16838twips"), "twips-based page size in mso conditional");
|
|
151
|
+
// Colgroups injected so fixed-layout tables have deterministic column widths
|
|
152
|
+
assert.ok(doc.includes('<colgroup>'), "colgroup injected on tables");
|
|
153
|
+
});
|
|
154
|
+
|
|
63
155
|
test("writeReports writes both files to disk", async () => {
|
|
64
156
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fad-checker-rep-"));
|
|
65
157
|
const r = await writeReports({
|