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 CHANGED
File without changes
File without changes
File without changes
@@ -1,6 +1,6 @@
1
1
  # Security Assessment
2
2
 
3
- Run a security scan and generate a PDF report. Execute automatically without asking questions.
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 the full security assessment:
11
+ Use ONE Task agent with subagent_type="general-purpose" to perform a COMPREHENSIVE assessment:
12
12
 
13
- prompt: "Perform a comprehensive security assessment of this codebase. Analyze: 1) Architecture and tech stack 2) API endpoints and attack surface 3) Security vulnerabilities (secrets, injection, XSS, auth issues) 4) Infrastructure (Docker, CI/CD, dependencies). When done, use the Write tool to create .coverme/scan.json with this structure: {\"project\":\"<name>\",\"date\":\"2026-02-19\",\"executiveSummary\":\"<summary>\",\"summary\":{\"critical\":0,\"high\":0,\"medium\":0,\"low\":0,\"total\":0},\"overallRiskLevel\":\"low|medium|high|critical\",\"findings\":[{\"id\":\"VULN-01\",\"title\":\"\",\"severity\":\"critical|high|medium|low\",\"file\":\"\",\"line\":0,\"issue\":\"\",\"why\":\"\",\"fix\":\"\",\"cwe\":\"\"}],\"architecture\":{\"overview\":\"\",\"components\":[]},\"positiveObservations\":[{\"title\":\"\",\"description\":\"\"}]}"
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
- ## Step 3: Wait for agent to complete
16
- Use AgentOutputTool to wait for the agent.
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
@@ -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 severity color, title in black
617
+ // Finding ID in accent color
618
618
  this.doc
619
619
  .font(fonts.weights.bold)
620
620
  .fontSize(fonts.sizes.body)
621
- .fillColor(style.text)
622
- .text(`[${finding.id}]`, spacing.page.margin, this.y, { continued: true })
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
- // Cross-references and DREAD score line
627
- const hasRefs = finding.relatedFindings?.length || finding.dreadScore || finding.cwe;
628
- if (hasRefs) {
629
- const parts = [];
630
- if (finding.dreadScore)
631
- parts.push(`DREAD: ${finding.dreadScore.toFixed(1)}`);
632
- if (finding.cwe)
633
- parts.push(finding.cwe);
634
- if (finding.relatedFindings?.length) {
635
- parts.push(`Related: ${finding.relatedFindings.join(', ')}`);
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.normal)
639
- .fontSize(fonts.sizes.small)
640
- .fillColor(colors.text.muted)
641
- .text(parts.join(' | '), spacing.page.margin, this.y);
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
- // File reference (if present)
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 += 18;
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 rowHeight = 24;
1181
+ const minRowHeight = 24;
1145
1182
  const x = spacing.page.margin;
1146
- // Header
1147
- this.doc
1148
- .rect(x, this.y, layout.content.width, rowHeight)
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
- .font(fonts.weights.bold)
1154
- .fontSize(fonts.sizes.small)
1155
- .fillColor(colors.table.headerText)
1156
- .text(header, colX + 8, this.y + 7, {
1157
- width: colWidths[i] - 16,
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
- colX += colWidths[i];
1160
- });
1161
- this.y += rowHeight;
1162
- // Rows
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.checkPageBreak(rowHeight + 20);
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, rowHeight)
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 + rowHeight)
1174
- .lineTo(x + layout.content.width, this.y + rowHeight)
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 + 8, this.y + 7, {
1185
- width: colWidths[i] - 16,
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 += rowHeight;
1247
+ this.y += maxCellHeight;
1191
1248
  });
1192
1249
  this.y += spacing.paragraph;
1193
1250
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coverme-security-scanner",
3
- "version": "3.7.3",
3
+ "version": "3.7.6",
4
4
  "description": "AI-powered security assessment reports with beautiful PDF output",
5
5
  "type": "module",
6
6
  "bin": {