coverme-security-scanner 3.7.3 → 3.7.6
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/bin/coverme.js +0 -0
- package/bin/install-command.js +0 -0
- package/bin/merge-reports.js +0 -0
- package/commands/coverme.md +276 -5
- package/dist/pdf/generator.js +106 -49
- package/package.json +1 -1
package/bin/coverme.js
CHANGED
|
File without changes
|
package/bin/install-command.js
CHANGED
|
File without changes
|
package/bin/merge-reports.js
CHANGED
|
File without changes
|
package/commands/coverme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Security Assessment
|
|
2
2
|
|
|
3
|
-
Run
|
|
3
|
+
Run an enterprise-grade security assessment combining 4 review types into a unified report. Execute automatically without asking questions.
|
|
4
4
|
|
|
5
5
|
## Step 1: Setup
|
|
6
6
|
```bash
|
|
@@ -8,12 +8,283 @@ mkdir -p .coverme
|
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
## Step 2: Run Security Scan
|
|
11
|
-
Use ONE Task agent with subagent_type="general-purpose" to perform
|
|
11
|
+
Use ONE Task agent with subagent_type="general-purpose" to perform a COMPREHENSIVE assessment:
|
|
12
12
|
|
|
13
|
-
prompt:
|
|
13
|
+
prompt: |
|
|
14
|
+
Perform a COMPREHENSIVE security assessment combining:
|
|
15
|
+
1. Security Review (STRIDE vulnerabilities)
|
|
16
|
+
2. Threat Model (DREAD scoring)
|
|
17
|
+
3. Code Quality Review (dead code, DRY violations)
|
|
18
|
+
4. Privacy Analysis (LINDDUN)
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
YOU MUST produce findings at the quality level of a professional security audit firm.
|
|
21
|
+
Target: 25-35 findings for a medium codebase, 40+ for large.
|
|
22
|
+
|
|
23
|
+
## PHASE 1: Codebase Metrics
|
|
24
|
+
|
|
25
|
+
First, gather precise metrics:
|
|
26
|
+
```
|
|
27
|
+
- Count total files: find . -type f \( -name "*.ts" -o -name "*.js" -o -name "*.tsx" -o -name "*.py" \) | wc -l
|
|
28
|
+
- Count lines: find . -type f \( -name "*.ts" -o -name "*.js" -o -name "*.tsx" -o -name "*.py" \) -exec wc -l {} + | tail -1
|
|
29
|
+
- List components (frontend/, backend/, api/, etc.)
|
|
30
|
+
- Map technology stack (React, Express, Postgres, etc.)
|
|
31
|
+
- Read .gitignore to know what's excluded
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## PHASE 2: Architecture Analysis
|
|
35
|
+
|
|
36
|
+
Map the system:
|
|
37
|
+
- Identify all components with their technology
|
|
38
|
+
- Define trust boundaries (TB1-Browser, TB2-BFF, TB3-API, TB4-Database, TB5-External)
|
|
39
|
+
- Document entry points with auth method
|
|
40
|
+
- List critical assets (keys, tokens, PII, credentials)
|
|
41
|
+
|
|
42
|
+
## PHASE 3: Security Vulnerabilities (STRIDE)
|
|
43
|
+
|
|
44
|
+
For EACH finding, you MUST include ALL of these:
|
|
45
|
+
|
|
46
|
+
**Required Fields:**
|
|
47
|
+
- **ID**: Use prefixes based on source:
|
|
48
|
+
- `T-FE-N`: Frontend threats
|
|
49
|
+
- `T-BE-N`: Backend threats
|
|
50
|
+
- `T-DB-N`: Database threats
|
|
51
|
+
- `T-ENC-N`: Encryption/enclave threats
|
|
52
|
+
- `CR-N`: Code review findings
|
|
53
|
+
- `SR-N`: Security review findings
|
|
54
|
+
- `QR-N`: Quality review items
|
|
55
|
+
|
|
56
|
+
- **Title**: Specific, not generic. Good: "Attestation Fallback Accepts Unverified Keys". Bad: "Security Issue"
|
|
57
|
+
|
|
58
|
+
- **File + Line Range**: Exact location with range. Example: `frontend/lib/e2e-encryption.ts:51-91`
|
|
59
|
+
|
|
60
|
+
- **Code References**: Include actual function/variable names inline:
|
|
61
|
+
- Good: "When unavailable, `fetchEnclaveInfo()` falls back and stores keys with `keysVerified: false`"
|
|
62
|
+
- Bad: "The function has a fallback"
|
|
63
|
+
|
|
64
|
+
- **DREAD Score**: Calculate each component (1-10):
|
|
65
|
+
- Damage: How bad if exploited?
|
|
66
|
+
- Reproducibility: How easy to reproduce?
|
|
67
|
+
- Exploitability: How easy to exploit?
|
|
68
|
+
- Affected Users: How many affected?
|
|
69
|
+
- Discoverability: How easy to find?
|
|
70
|
+
- Final: (D+R+E+A+D) / 5
|
|
71
|
+
|
|
72
|
+
- **Description**: 2-3 sentences explaining WHAT the code does wrong with inline `code references`
|
|
73
|
+
|
|
74
|
+
- **Recommendation**: Specific actionable fix, not generic advice
|
|
75
|
+
|
|
76
|
+
- **Status**: `open` | `partial` | `mitigated` | `accepted`
|
|
77
|
+
|
|
78
|
+
- **CWE**: Include CWE identifier (e.g., "CWE-295: Improper Certificate Validation")
|
|
79
|
+
|
|
80
|
+
- **relatedFindings**: Link to related findings when issues are connected
|
|
81
|
+
|
|
82
|
+
### Vulnerability Categories to Check:
|
|
83
|
+
|
|
84
|
+
**Silent Fallbacks (CRITICAL priority)**:
|
|
85
|
+
- try/catch blocks that swallow errors and continue execution
|
|
86
|
+
- `||` operators providing insecure defaults (e.g., `password || 'default123'`)
|
|
87
|
+
- Authentication that defaults to "allow" on error
|
|
88
|
+
- Crypto operations falling back to weaker algorithms
|
|
89
|
+
- if/else branches that silently skip security checks
|
|
90
|
+
- Graceful degradation that removes security controls
|
|
91
|
+
|
|
92
|
+
**Hardcoded Credentials**:
|
|
93
|
+
- Default passwords in code (not just comments)
|
|
94
|
+
- API keys in source (not from env vars)
|
|
95
|
+
- Fallback credentials like `amqp://user:pass123@localhost`
|
|
96
|
+
- Secrets in Helm values, Docker configs, CI files
|
|
97
|
+
|
|
98
|
+
**Input Validation Gaps**:
|
|
99
|
+
- User input used in SQL/shell/eval without sanitization
|
|
100
|
+
- Missing regex validation on IDs, names, paths
|
|
101
|
+
- Unbounded arrays that could cause DoS
|
|
102
|
+
- Path traversal vulnerabilities
|
|
103
|
+
|
|
104
|
+
**Error Information Leaks**:
|
|
105
|
+
- `error.message` or `error.stack` returned to clients
|
|
106
|
+
- Internal hostnames/IPs in error responses
|
|
107
|
+
- Database errors exposed to users
|
|
108
|
+
- Debug info in production responses
|
|
109
|
+
|
|
110
|
+
**Authentication/Authorization**:
|
|
111
|
+
- Missing auth on sensitive endpoints
|
|
112
|
+
- Token validation issues
|
|
113
|
+
- Session management flaws
|
|
114
|
+
- Privilege escalation paths
|
|
115
|
+
|
|
116
|
+
**Cryptographic Issues**:
|
|
117
|
+
- Weak algorithms (MD5, SHA1 for security)
|
|
118
|
+
- Hardcoded IVs or keys
|
|
119
|
+
- Missing signature verification
|
|
120
|
+
- Key management flaws
|
|
121
|
+
|
|
122
|
+
## PHASE 4: Code Quality Review
|
|
123
|
+
|
|
124
|
+
Find issues with LINE COUNTS:
|
|
125
|
+
|
|
126
|
+
**Dead Code (estimate removable lines)**:
|
|
127
|
+
- Functions defined but never called (trace call graph)
|
|
128
|
+
- Routes defined but not mounted
|
|
129
|
+
- Commented-out code blocks
|
|
130
|
+
- Example: "processMessage() - likely dead non-streaming path (~240 lines)"
|
|
131
|
+
|
|
132
|
+
**DRY Violations (estimate duplicated lines)**:
|
|
133
|
+
- Near-identical code blocks in multiple files
|
|
134
|
+
- Copy-pasted logic with minor variations
|
|
135
|
+
- Example: "CSV + Excel Handler process/prepareForStreaming nearly identical (~470 lines)"
|
|
136
|
+
|
|
137
|
+
**Deprecated Patterns**:
|
|
138
|
+
- Old API usage
|
|
139
|
+
- Legacy code paths
|
|
140
|
+
- Outdated dependencies
|
|
141
|
+
|
|
142
|
+
## PHASE 5: Privacy Analysis (LINDDUN)
|
|
143
|
+
|
|
144
|
+
Analyze for privacy threats:
|
|
145
|
+
- **L**inkability: Can actions be linked to users?
|
|
146
|
+
- **I**dentifiability: Can users be identified?
|
|
147
|
+
- **N**on-repudiation: Can users deny actions?
|
|
148
|
+
- **D**etectability: Can user presence be detected?
|
|
149
|
+
- **D**isclosure: Is data disclosed inappropriately?
|
|
150
|
+
- **U**nawareness: Are users unaware of data collection?
|
|
151
|
+
- **N**on-compliance: Are regulations violated?
|
|
152
|
+
|
|
153
|
+
## PHASE 6: Cross-Reference & Merge
|
|
154
|
+
|
|
155
|
+
**Merge duplicate findings**:
|
|
156
|
+
- When same issue found in multiple analyses, combine them
|
|
157
|
+
- Use format: "CR-02 / T-EKS-3: Hardcoded Tracker API Keys"
|
|
158
|
+
|
|
159
|
+
**Link related findings**:
|
|
160
|
+
- Add `relatedFindings: ["T-FE-2"]` when issues are connected
|
|
161
|
+
- Build attack chains showing how findings combine
|
|
162
|
+
|
|
163
|
+
## PHASE 7: Identify Resolved Issues
|
|
164
|
+
|
|
165
|
+
Look for issues that WERE present but are NOW fixed:
|
|
166
|
+
- Commented code showing old vulnerable patterns
|
|
167
|
+
- Security controls that are properly implemented
|
|
168
|
+
- Previous findings that have been addressed
|
|
169
|
+
|
|
170
|
+
Document these in `resolvedIssues` with:
|
|
171
|
+
- What was the original issue
|
|
172
|
+
- How it was resolved
|
|
173
|
+
- Current security status
|
|
174
|
+
|
|
175
|
+
## OUTPUT FORMAT
|
|
176
|
+
|
|
177
|
+
Create `.coverme/scan.json` with this EXACT structure:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"project": "<project-name>",
|
|
182
|
+
"date": "2026-02-19",
|
|
183
|
+
"branch": "main",
|
|
184
|
+
"scope": "<X> files, ~<Y> lines - Full platform assessment",
|
|
185
|
+
"components": ["Frontend (Next.js)", "Backend (Express)", "Database (Postgres)"],
|
|
186
|
+
"methodology": "STRIDE + DREAD-D + LINDDUN",
|
|
187
|
+
"reviewType": "Security + Threat Model + Code Quality + Privacy",
|
|
188
|
+
"author": "Claude Code",
|
|
189
|
+
|
|
190
|
+
"executiveSummary": "<3-4 paragraphs: 1) System overview 2) Security architecture 3) Key findings summary 4) Critical remaining issues>",
|
|
191
|
+
"overallRiskLevel": "low|medium|high|critical",
|
|
192
|
+
|
|
193
|
+
"summary": { "critical": 0, "high": 0, "medium": 0, "low": 0, "total": 0 },
|
|
194
|
+
|
|
195
|
+
"topPriorities": [
|
|
196
|
+
{ "finding": "T-FE-1", "severity": "high", "action": "Block attestation fallback" }
|
|
197
|
+
],
|
|
198
|
+
|
|
199
|
+
"architecture": {
|
|
200
|
+
"overview": "<1-2 sentences about system architecture>",
|
|
201
|
+
"components": [
|
|
202
|
+
{"name": "Frontend", "technology": "Next.js BFF", "description": "PKCE OAuth, session management"}
|
|
203
|
+
],
|
|
204
|
+
"trustBoundaries": [
|
|
205
|
+
{"id": "TB1", "boundary": "Browser", "trustLevel": "untrusted", "description": "User browser environment"},
|
|
206
|
+
{"id": "TB2", "boundary": "API Gateway", "trustLevel": "semi-trusted", "description": "Holds OAuth tokens"}
|
|
207
|
+
]
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
"findings": [
|
|
211
|
+
{
|
|
212
|
+
"id": "T-FE-1",
|
|
213
|
+
"title": "Attestation Fallback Accepts Unverified Keys",
|
|
214
|
+
"severity": "high",
|
|
215
|
+
"status": "open",
|
|
216
|
+
"file": "frontend/lib/e2e-encryption.ts",
|
|
217
|
+
"line": "51-91",
|
|
218
|
+
"dreadScore": 6.3,
|
|
219
|
+
"cwe": "CWE-295: Improper Certificate Validation",
|
|
220
|
+
"issue": "When the attestation bundle endpoint is unavailable, `fetchEnclaveInfo()` falls back to the legacy `/api/v1/enclave` endpoint and stores keys with `keysVerified: false`. The browser silently proceeds to encrypt messages with unverified public keys.",
|
|
221
|
+
"why": "This enables a man-in-the-middle attack by a compromised gateway. An attacker who controls the gateway can substitute their own keys and decrypt all messages.",
|
|
222
|
+
"fix": "Block the request or display a prominent security warning when attestation fails. Do not silently proceed with unverified keys. Consider: `if (!keysVerified) throw new AttestationError('Cannot proceed without verified keys')`",
|
|
223
|
+
"relatedFindings": ["T-FE-2", "CR-07"]
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
|
|
227
|
+
"threatModel": [
|
|
228
|
+
{"id": "T-FE-1", "severity": "high", "dread": 6.3, "status": "partial", "finding": "Attestation fallback accepts unverified keys"},
|
|
229
|
+
{"id": "T-ENC-2", "severity": "low", "dread": 2.0, "status": "mitigated", "finding": "Session key zeroization implemented"}
|
|
230
|
+
],
|
|
231
|
+
|
|
232
|
+
"qualityReview": {
|
|
233
|
+
"deadCode": [
|
|
234
|
+
{"type": "dead-code", "action": "DELETE", "file": "src/old-handler.js", "line": 1, "description": "processMessage() - likely dead non-streaming path (~240 lines)"}
|
|
235
|
+
],
|
|
236
|
+
"dryViolations": [
|
|
237
|
+
{"type": "dry-violation", "action": "MERGE", "file": "src/handlers/csv.js", "description": "CSV + Excel Handler process/prepareForStreaming nearly identical (~470 lines)", "relatedFiles": ["src/handlers/excel.js"]}
|
|
238
|
+
]
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
"positiveObservations": [
|
|
242
|
+
{"title": "Zero-Knowledge Architecture", "description": "API gateway genuinely never sees plaintext. Encrypted payloads flow through without decryption."},
|
|
243
|
+
{"title": "Atomic Credit Operations", "description": "Lua scripts for token burning prevent cross-pod race conditions."},
|
|
244
|
+
{"title": "Post-Quantum Cryptography", "description": "XWing (ML-KEM-768 + X25519) hybrid KEM with Ed25519 signing."}
|
|
245
|
+
],
|
|
246
|
+
|
|
247
|
+
"resolvedIssues": [
|
|
248
|
+
{"id": "RES-01", "title": "SQL Injection in DuckDB", "severity": "critical", "resolution": "Resolved: enable_external_access=false sandbox mode + SQL validation blocklist."},
|
|
249
|
+
{"id": "RES-02", "title": "Admin API Fail-Open", "severity": "high", "resolution": "Resolved: Binds to 127.0.0.1 by default. Fail-closed when no ADMIN_ALLOWED_IPS configured."}
|
|
250
|
+
],
|
|
251
|
+
|
|
252
|
+
"privacyAnalysis": [
|
|
253
|
+
{"category": "Linkability", "risk": "medium", "description": "User actions can be linked via hashed KRN in metrics", "mitigation": "SHA-256 hashing provides pseudonymization"},
|
|
254
|
+
{"category": "Identifiability", "risk": "low", "description": "KRN logged in some paths enables identification", "mitigation": "Replace with getUserHash() in all logs"}
|
|
255
|
+
],
|
|
256
|
+
|
|
257
|
+
"remediation": {
|
|
258
|
+
"p0": [{"action": "Remove hardcoded secrets from Helm values. Move to Secrets Manager. Rotate keys.", "finding": "CR-02", "owner": "DevOps"}],
|
|
259
|
+
"p1": [{"action": "Block attestation fallback or show prominent warning", "finding": "T-FE-1", "owner": "Frontend"}],
|
|
260
|
+
"p2": [{"action": "Extract Redis metrics to shared module (~160 lines)", "finding": "QR-16", "owner": "Backend"}]
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## QUALITY REQUIREMENTS
|
|
266
|
+
|
|
267
|
+
**Minimum Thresholds:**
|
|
268
|
+
- 25+ findings for medium codebase (5k-50k lines)
|
|
269
|
+
- 40+ findings for large codebase (50k+ lines)
|
|
270
|
+
- Every HIGH/CRITICAL finding MUST have exact file:line
|
|
271
|
+
- Every finding MUST have DREAD score
|
|
272
|
+
- Every finding MUST have CWE identifier
|
|
273
|
+
- Include at least 5 positive observations
|
|
274
|
+
- Include code quality items with line counts
|
|
275
|
+
- Cross-reference related findings
|
|
276
|
+
- Include at least 3 resolved issues (if any exist)
|
|
277
|
+
- Include LINDDUN privacy analysis
|
|
278
|
+
|
|
279
|
+
**Quality Checks:**
|
|
280
|
+
- NO generic findings like "Consider adding validation"
|
|
281
|
+
- NO findings without file locations
|
|
282
|
+
- NO DREAD scores without calculation
|
|
283
|
+
- ALL code references must use actual function/variable names from the code
|
|
284
|
+
- ALL recommendations must be actionable, not vague
|
|
285
|
+
|
|
286
|
+
## Step 3: Wait for agent
|
|
287
|
+
Use AgentOutputTool to wait for completion.
|
|
17
288
|
|
|
18
289
|
## Step 4: Generate PDF
|
|
19
290
|
```bash
|
package/dist/pdf/generator.js
CHANGED
|
@@ -402,6 +402,7 @@ export class PDFGenerator {
|
|
|
402
402
|
this.y = this.doc.y + spacing.paragraph;
|
|
403
403
|
}
|
|
404
404
|
if (report.architecture?.components?.length) {
|
|
405
|
+
this.checkPageBreak(150);
|
|
405
406
|
this.subTitle('Components');
|
|
406
407
|
this.renderSimpleTable(['Component', 'Technology', 'Description'], report.architecture.components.map(c => [c.name, c.technology, c.description]), [100, 150, 245]);
|
|
407
408
|
}
|
|
@@ -434,7 +435,6 @@ export class PDFGenerator {
|
|
|
434
435
|
this.y += 18;
|
|
435
436
|
});
|
|
436
437
|
}
|
|
437
|
-
this.checkPageBreak();
|
|
438
438
|
}
|
|
439
439
|
// ─────────────────────────────────────────────────────────────────
|
|
440
440
|
// Network
|
|
@@ -614,41 +614,78 @@ export class PDFGenerator {
|
|
|
614
614
|
const boxPadding = 12;
|
|
615
615
|
const boxInnerWidth = layout.content.width - (boxPadding * 2);
|
|
616
616
|
const style = colors.severity[finding.severity];
|
|
617
|
-
// ID in
|
|
617
|
+
// Finding ID in accent color
|
|
618
618
|
this.doc
|
|
619
619
|
.font(fonts.weights.bold)
|
|
620
620
|
.fontSize(fonts.sizes.body)
|
|
621
|
-
.fillColor(
|
|
622
|
-
.text(
|
|
623
|
-
.fillColor(colors.text.primary)
|
|
624
|
-
.text(` ${finding.title}`);
|
|
621
|
+
.fillColor(colors.accent)
|
|
622
|
+
.text(finding.id, spacing.page.margin, this.y);
|
|
625
623
|
this.y += 18;
|
|
626
|
-
//
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
624
|
+
// Title - large and prominent
|
|
625
|
+
this.doc
|
|
626
|
+
.font(fonts.weights.bold)
|
|
627
|
+
.fontSize(fonts.sizes.h3)
|
|
628
|
+
.fillColor(colors.text.primary)
|
|
629
|
+
.text(finding.title, spacing.page.margin, this.y, { width: layout.content.width });
|
|
630
|
+
this.y = this.doc.y + 6;
|
|
631
|
+
// Severity badge (small pill)
|
|
632
|
+
const badgeHeight = 16;
|
|
633
|
+
const severityText = finding.severity.toUpperCase();
|
|
634
|
+
const severityWidth = this.doc.font(fonts.weights.bold).fontSize(7).widthOfString(severityText) + 12;
|
|
635
|
+
this.doc
|
|
636
|
+
.roundedRect(spacing.page.margin, this.y, severityWidth, badgeHeight, 3)
|
|
637
|
+
.fill(style.bg);
|
|
638
|
+
this.doc
|
|
639
|
+
.font(fonts.weights.bold)
|
|
640
|
+
.fontSize(7)
|
|
641
|
+
.fillColor(style.text)
|
|
642
|
+
.text(severityText, spacing.page.margin + 6, this.y + 4);
|
|
643
|
+
let xOffset = spacing.page.margin + severityWidth + 6;
|
|
644
|
+
// Status badge (if present) - small colored text, no box
|
|
645
|
+
if (finding.status) {
|
|
646
|
+
const statusColors = {
|
|
647
|
+
open: '#DC2626',
|
|
648
|
+
partial: '#D97706',
|
|
649
|
+
mitigated: '#059669',
|
|
650
|
+
accepted: '#4F46E5',
|
|
651
|
+
resolved: '#059669',
|
|
652
|
+
};
|
|
653
|
+
const statusColor = statusColors[finding.status] || statusColors.open;
|
|
654
|
+
const statusText = finding.status.charAt(0).toUpperCase() + finding.status.slice(1);
|
|
637
655
|
this.doc
|
|
638
|
-
.font(fonts.weights.
|
|
639
|
-
.fontSize(
|
|
640
|
-
.fillColor(
|
|
641
|
-
.text(
|
|
642
|
-
this.y += 14;
|
|
656
|
+
.font(fonts.weights.bold)
|
|
657
|
+
.fontSize(9)
|
|
658
|
+
.fillColor(statusColor)
|
|
659
|
+
.text(statusText, xOffset, this.y + 3);
|
|
643
660
|
}
|
|
644
|
-
|
|
661
|
+
this.y += badgeHeight + 8;
|
|
662
|
+
// File reference with line numbers (if present)
|
|
645
663
|
if (finding.file) {
|
|
646
664
|
this.doc
|
|
647
665
|
.font(fonts.mono)
|
|
648
666
|
.fontSize(fonts.sizes.small)
|
|
649
667
|
.fillColor(colors.accent)
|
|
650
668
|
.text(`${finding.file}${finding.line ? `:${finding.line}` : ''}`, spacing.page.margin, this.y);
|
|
651
|
-
this.y +=
|
|
669
|
+
this.y += 16;
|
|
670
|
+
}
|
|
671
|
+
// DREAD/CVSS score and metadata line
|
|
672
|
+
const metaParts = [];
|
|
673
|
+
if (finding.dreadScore)
|
|
674
|
+
metaParts.push(`DREAD: ${finding.dreadScore.toFixed(1)}`);
|
|
675
|
+
if (finding.cvssScore)
|
|
676
|
+
metaParts.push(`CVSS: ${finding.cvssScore.toFixed(1)}`);
|
|
677
|
+
if (finding.cwe)
|
|
678
|
+
metaParts.push(finding.cwe);
|
|
679
|
+
if (finding.relatedFindings?.length) {
|
|
680
|
+
metaParts.push(`Related: ${finding.relatedFindings.join(', ')}`);
|
|
681
|
+
}
|
|
682
|
+
if (metaParts.length > 0) {
|
|
683
|
+
this.doc
|
|
684
|
+
.font(fonts.weights.normal)
|
|
685
|
+
.fontSize(fonts.sizes.small)
|
|
686
|
+
.fillColor(colors.text.muted)
|
|
687
|
+
.text(metaParts.join(' | '), spacing.page.margin, this.y);
|
|
688
|
+
this.y += 16;
|
|
652
689
|
}
|
|
653
690
|
// ─────────────────────────────────────────────────────────────
|
|
654
691
|
// Check for structured format (issue/why/fix) vs legacy format
|
|
@@ -1141,53 +1178,73 @@ export class PDFGenerator {
|
|
|
1141
1178
|
this.y += 20;
|
|
1142
1179
|
}
|
|
1143
1180
|
renderSimpleTable(headers, rows, colWidths) {
|
|
1144
|
-
const
|
|
1181
|
+
const minRowHeight = 24;
|
|
1145
1182
|
const x = spacing.page.margin;
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
.fill(colors.table.header);
|
|
1150
|
-
let colX = x;
|
|
1151
|
-
headers.forEach((header, i) => {
|
|
1183
|
+
const cellPadding = 8;
|
|
1184
|
+
// Helper to render table header
|
|
1185
|
+
const renderHeader = () => {
|
|
1152
1186
|
this.doc
|
|
1153
|
-
.
|
|
1154
|
-
.
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1187
|
+
.rect(x, this.y, layout.content.width, minRowHeight)
|
|
1188
|
+
.fill(colors.table.header);
|
|
1189
|
+
let colX = x;
|
|
1190
|
+
headers.forEach((header, i) => {
|
|
1191
|
+
this.doc
|
|
1192
|
+
.font(fonts.weights.bold)
|
|
1193
|
+
.fontSize(fonts.sizes.small)
|
|
1194
|
+
.fillColor(colors.table.headerText)
|
|
1195
|
+
.text(header, colX + cellPadding, this.y + 7, {
|
|
1196
|
+
width: colWidths[i] - (cellPadding * 2),
|
|
1197
|
+
});
|
|
1198
|
+
colX += colWidths[i];
|
|
1158
1199
|
});
|
|
1159
|
-
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1200
|
+
this.y += minRowHeight;
|
|
1201
|
+
};
|
|
1202
|
+
// Initial header
|
|
1203
|
+
renderHeader();
|
|
1204
|
+
// Rows - calculate dynamic height based on content
|
|
1163
1205
|
rows.forEach((row, rowIndex) => {
|
|
1164
|
-
this
|
|
1206
|
+
// Calculate the height needed for this row based on longest cell
|
|
1207
|
+
let maxCellHeight = minRowHeight;
|
|
1208
|
+
row.forEach((cell, i) => {
|
|
1209
|
+
const cellWidth = colWidths[i] - (cellPadding * 2);
|
|
1210
|
+
const textHeight = this.doc
|
|
1211
|
+
.font(fonts.weights.normal)
|
|
1212
|
+
.fontSize(fonts.sizes.small)
|
|
1213
|
+
.heightOfString(cell, { width: cellWidth });
|
|
1214
|
+
const cellHeight = Math.max(minRowHeight, textHeight + (cellPadding * 2));
|
|
1215
|
+
maxCellHeight = Math.max(maxCellHeight, cellHeight);
|
|
1216
|
+
});
|
|
1217
|
+
// Check if we need a page break - if so, re-render header on new page
|
|
1218
|
+
const needsPageBreak = this.y > layout.page.height - spacing.page.bottom - maxCellHeight - 20;
|
|
1219
|
+
if (needsPageBreak) {
|
|
1220
|
+
this.newPage();
|
|
1221
|
+
renderHeader();
|
|
1222
|
+
}
|
|
1165
1223
|
// Alternating background
|
|
1166
1224
|
if (rowIndex % 2 === 1) {
|
|
1167
1225
|
this.doc
|
|
1168
|
-
.rect(x, this.y, layout.content.width,
|
|
1226
|
+
.rect(x, this.y, layout.content.width, maxCellHeight)
|
|
1169
1227
|
.fill(colors.table.altRow);
|
|
1170
1228
|
}
|
|
1171
1229
|
// Border
|
|
1172
1230
|
this.doc
|
|
1173
|
-
.moveTo(x, this.y +
|
|
1174
|
-
.lineTo(x + layout.content.width, this.y +
|
|
1231
|
+
.moveTo(x, this.y + maxCellHeight)
|
|
1232
|
+
.lineTo(x + layout.content.width, this.y + maxCellHeight)
|
|
1175
1233
|
.strokeColor(colors.table.border)
|
|
1176
1234
|
.lineWidth(0.5)
|
|
1177
1235
|
.stroke();
|
|
1178
|
-
colX = x;
|
|
1236
|
+
let colX = x;
|
|
1179
1237
|
row.forEach((cell, i) => {
|
|
1180
1238
|
this.doc
|
|
1181
1239
|
.font(fonts.weights.normal)
|
|
1182
1240
|
.fontSize(fonts.sizes.small)
|
|
1183
1241
|
.fillColor(colors.text.primary)
|
|
1184
|
-
.text(cell, colX +
|
|
1185
|
-
width: colWidths[i] -
|
|
1186
|
-
ellipsis: true,
|
|
1242
|
+
.text(cell, colX + cellPadding, this.y + 7, {
|
|
1243
|
+
width: colWidths[i] - (cellPadding * 2),
|
|
1187
1244
|
});
|
|
1188
1245
|
colX += colWidths[i];
|
|
1189
1246
|
});
|
|
1190
|
-
this.y +=
|
|
1247
|
+
this.y += maxCellHeight;
|
|
1191
1248
|
});
|
|
1192
1249
|
this.y += spacing.paragraph;
|
|
1193
1250
|
}
|