api-tests-coverage 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/README.md +703 -0
  2. package/config.yaml.example +227 -0
  3. package/dist/action/src/index.d.ts +2 -0
  4. package/dist/action/src/index.d.ts.map +1 -0
  5. package/dist/action/src/index.js +349 -0
  6. package/dist/action/src/prComment.d.ts +34 -0
  7. package/dist/action/src/prComment.d.ts.map +1 -0
  8. package/dist/action/src/prComment.js +146 -0
  9. package/dist/src/ast/astAnalysisOrchestrator.d.ts +36 -0
  10. package/dist/src/ast/astAnalysisOrchestrator.d.ts.map +1 -0
  11. package/dist/src/ast/astAnalysisOrchestrator.js +123 -0
  12. package/dist/src/ast/astTypes.d.ts +105 -0
  13. package/dist/src/ast/astTypes.d.ts.map +1 -0
  14. package/dist/src/ast/astTypes.js +9 -0
  15. package/dist/src/ast/languageAnalyzer.d.ts +46 -0
  16. package/dist/src/ast/languageAnalyzer.d.ts.map +1 -0
  17. package/dist/src/ast/languageAnalyzer.js +9 -0
  18. package/dist/src/ast/languageCapabilities.d.ts +24 -0
  19. package/dist/src/ast/languageCapabilities.d.ts.map +1 -0
  20. package/dist/src/ast/languageCapabilities.js +92 -0
  21. package/dist/src/ast/parseFile.d.ts +16 -0
  22. package/dist/src/ast/parseFile.d.ts.map +1 -0
  23. package/dist/src/ast/parseFile.js +65 -0
  24. package/dist/src/ast/parserRegistry.d.ts +39 -0
  25. package/dist/src/ast/parserRegistry.d.ts.map +1 -0
  26. package/dist/src/ast/parserRegistry.js +66 -0
  27. package/dist/src/buildSummary.d.ts +26 -0
  28. package/dist/src/buildSummary.d.ts.map +1 -0
  29. package/dist/src/buildSummary.js +193 -0
  30. package/dist/src/businessCoverage.d.ts +68 -0
  31. package/dist/src/businessCoverage.d.ts.map +1 -0
  32. package/dist/src/businessCoverage.js +290 -0
  33. package/dist/src/compatibilityCoverage.d.ts +83 -0
  34. package/dist/src/compatibilityCoverage.d.ts.map +1 -0
  35. package/dist/src/compatibilityCoverage.js +501 -0
  36. package/dist/src/config/defaultConfig.d.ts +9 -0
  37. package/dist/src/config/defaultConfig.d.ts.map +1 -0
  38. package/dist/src/config/defaultConfig.js +97 -0
  39. package/dist/src/config/index.d.ts +12 -0
  40. package/dist/src/config/index.d.ts.map +1 -0
  41. package/dist/src/config/index.js +37 -0
  42. package/dist/src/config/loadConfig.d.ts +29 -0
  43. package/dist/src/config/loadConfig.d.ts.map +1 -0
  44. package/dist/src/config/loadConfig.js +135 -0
  45. package/dist/src/config/mergeConfig.d.ts +15 -0
  46. package/dist/src/config/mergeConfig.d.ts.map +1 -0
  47. package/dist/src/config/mergeConfig.js +57 -0
  48. package/dist/src/config/schema.d.ts +15 -0
  49. package/dist/src/config/schema.d.ts.map +1 -0
  50. package/dist/src/config/schema.js +30 -0
  51. package/dist/src/config/types.d.ts +175 -0
  52. package/dist/src/config/types.d.ts.map +1 -0
  53. package/dist/src/config/types.js +9 -0
  54. package/dist/src/config/validateConfig.d.ts +22 -0
  55. package/dist/src/config/validateConfig.d.ts.map +1 -0
  56. package/dist/src/config/validateConfig.js +171 -0
  57. package/dist/src/config.d.ts +168 -0
  58. package/dist/src/config.d.ts.map +1 -0
  59. package/dist/src/config.js +204 -0
  60. package/dist/src/coverage/deep-analysis/callGraph.d.ts +67 -0
  61. package/dist/src/coverage/deep-analysis/callGraph.d.ts.map +1 -0
  62. package/dist/src/coverage/deep-analysis/callGraph.js +275 -0
  63. package/dist/src/coverage/deep-analysis/deepEndpointResolver.d.ts +23 -0
  64. package/dist/src/coverage/deep-analysis/deepEndpointResolver.d.ts.map +1 -0
  65. package/dist/src/coverage/deep-analysis/deepEndpointResolver.js +394 -0
  66. package/dist/src/coverage/deep-analysis/index.d.ts +17 -0
  67. package/dist/src/coverage/deep-analysis/index.d.ts.map +1 -0
  68. package/dist/src/coverage/deep-analysis/index.js +63 -0
  69. package/dist/src/coverage/deep-analysis/resolveAssertions.d.ts +60 -0
  70. package/dist/src/coverage/deep-analysis/resolveAssertions.d.ts.map +1 -0
  71. package/dist/src/coverage/deep-analysis/resolveAssertions.js +121 -0
  72. package/dist/src/coverage/deep-analysis/resolveConstants.d.ts +36 -0
  73. package/dist/src/coverage/deep-analysis/resolveConstants.d.ts.map +1 -0
  74. package/dist/src/coverage/deep-analysis/resolveConstants.js +92 -0
  75. package/dist/src/coverage/deep-analysis/resolveEnums.d.ts +55 -0
  76. package/dist/src/coverage/deep-analysis/resolveEnums.d.ts.map +1 -0
  77. package/dist/src/coverage/deep-analysis/resolveEnums.js +152 -0
  78. package/dist/src/coverage/deep-analysis/resolveMethodChains.d.ts +70 -0
  79. package/dist/src/coverage/deep-analysis/resolveMethodChains.d.ts.map +1 -0
  80. package/dist/src/coverage/deep-analysis/resolveMethodChains.js +152 -0
  81. package/dist/src/coverage/deep-analysis/resolvePaths.d.ts +80 -0
  82. package/dist/src/coverage/deep-analysis/resolvePaths.d.ts.map +1 -0
  83. package/dist/src/coverage/deep-analysis/resolvePaths.js +216 -0
  84. package/dist/src/coverage/deep-analysis/resolveRequestWrappers.d.ts +71 -0
  85. package/dist/src/coverage/deep-analysis/resolveRequestWrappers.d.ts.map +1 -0
  86. package/dist/src/coverage/deep-analysis/resolveRequestWrappers.js +226 -0
  87. package/dist/src/coverage/deep-analysis/symbolTable.d.ts +58 -0
  88. package/dist/src/coverage/deep-analysis/symbolTable.d.ts.map +1 -0
  89. package/dist/src/coverage/deep-analysis/symbolTable.js +230 -0
  90. package/dist/src/coverage/deep-analysis/types.d.ts +122 -0
  91. package/dist/src/coverage/deep-analysis/types.d.ts.map +1 -0
  92. package/dist/src/coverage/deep-analysis/types.js +21 -0
  93. package/dist/src/discovery/fileClassifier.d.ts +50 -0
  94. package/dist/src/discovery/fileClassifier.d.ts.map +1 -0
  95. package/dist/src/discovery/fileClassifier.js +238 -0
  96. package/dist/src/discovery/projectDiscovery.d.ts +66 -0
  97. package/dist/src/discovery/projectDiscovery.d.ts.map +1 -0
  98. package/dist/src/discovery/projectDiscovery.js +287 -0
  99. package/dist/src/endpointCoverage.d.ts +70 -0
  100. package/dist/src/endpointCoverage.d.ts.map +1 -0
  101. package/dist/src/endpointCoverage.js +381 -0
  102. package/dist/src/errorCoverage.d.ts +93 -0
  103. package/dist/src/errorCoverage.d.ts.map +1 -0
  104. package/dist/src/errorCoverage.js +698 -0
  105. package/dist/src/index.d.ts +3 -0
  106. package/dist/src/index.d.ts.map +1 -0
  107. package/dist/src/index.js +1441 -0
  108. package/dist/src/inference/businessRuleInference.d.ts +63 -0
  109. package/dist/src/inference/businessRuleInference.d.ts.map +1 -0
  110. package/dist/src/inference/businessRuleInference.js +268 -0
  111. package/dist/src/inference/integrationFlowInference.d.ts +56 -0
  112. package/dist/src/inference/integrationFlowInference.d.ts.map +1 -0
  113. package/dist/src/inference/integrationFlowInference.js +266 -0
  114. package/dist/src/integrationCoverage.d.ts +72 -0
  115. package/dist/src/integrationCoverage.d.ts.map +1 -0
  116. package/dist/src/integrationCoverage.js +317 -0
  117. package/dist/src/intelligence/index.d.ts +20 -0
  118. package/dist/src/intelligence/index.d.ts.map +1 -0
  119. package/dist/src/intelligence/index.js +105 -0
  120. package/dist/src/intelligence/linkageEngine.d.ts +20 -0
  121. package/dist/src/intelligence/linkageEngine.d.ts.map +1 -0
  122. package/dist/src/intelligence/linkageEngine.js +522 -0
  123. package/dist/src/intelligence/markdownReporter.d.ts +12 -0
  124. package/dist/src/intelligence/markdownReporter.d.ts.map +1 -0
  125. package/dist/src/intelligence/markdownReporter.js +265 -0
  126. package/dist/src/intelligence/riskScoring.d.ts +53 -0
  127. package/dist/src/intelligence/riskScoring.d.ts.map +1 -0
  128. package/dist/src/intelligence/riskScoring.js +181 -0
  129. package/dist/src/intelligence/types.d.ts +121 -0
  130. package/dist/src/intelligence/types.d.ts.map +1 -0
  131. package/dist/src/intelligence/types.js +8 -0
  132. package/dist/src/languageDetection.d.ts +100 -0
  133. package/dist/src/languageDetection.d.ts.map +1 -0
  134. package/dist/src/languageDetection.js +349 -0
  135. package/dist/src/languages/java/index.d.ts +16 -0
  136. package/dist/src/languages/java/index.d.ts.map +1 -0
  137. package/dist/src/languages/java/index.js +103 -0
  138. package/dist/src/languages/java/parser.d.ts +7 -0
  139. package/dist/src/languages/java/parser.d.ts.map +1 -0
  140. package/dist/src/languages/java/parser.js +50 -0
  141. package/dist/src/languages/java/semanticBuilder.d.ts +21 -0
  142. package/dist/src/languages/java/semanticBuilder.d.ts.map +1 -0
  143. package/dist/src/languages/java/semanticBuilder.js +358 -0
  144. package/dist/src/languages/javascript/annotationExtractor.d.ts +20 -0
  145. package/dist/src/languages/javascript/annotationExtractor.d.ts.map +1 -0
  146. package/dist/src/languages/javascript/annotationExtractor.js +94 -0
  147. package/dist/src/languages/javascript/assertionResolver.d.ts +18 -0
  148. package/dist/src/languages/javascript/assertionResolver.d.ts.map +1 -0
  149. package/dist/src/languages/javascript/assertionResolver.js +150 -0
  150. package/dist/src/languages/javascript/callResolver.d.ts +23 -0
  151. package/dist/src/languages/javascript/callResolver.d.ts.map +1 -0
  152. package/dist/src/languages/javascript/callResolver.js +236 -0
  153. package/dist/src/languages/javascript/httpInteractionExtractor.d.ts +23 -0
  154. package/dist/src/languages/javascript/httpInteractionExtractor.d.ts.map +1 -0
  155. package/dist/src/languages/javascript/httpInteractionExtractor.js +205 -0
  156. package/dist/src/languages/javascript/index.d.ts +20 -0
  157. package/dist/src/languages/javascript/index.d.ts.map +1 -0
  158. package/dist/src/languages/javascript/index.js +136 -0
  159. package/dist/src/languages/javascript/parser.d.ts +14 -0
  160. package/dist/src/languages/javascript/parser.d.ts.map +1 -0
  161. package/dist/src/languages/javascript/parser.js +38 -0
  162. package/dist/src/languages/javascript/symbolResolver.d.ts +31 -0
  163. package/dist/src/languages/javascript/symbolResolver.d.ts.map +1 -0
  164. package/dist/src/languages/javascript/symbolResolver.js +183 -0
  165. package/dist/src/languages/kotlin/index.d.ts +16 -0
  166. package/dist/src/languages/kotlin/index.d.ts.map +1 -0
  167. package/dist/src/languages/kotlin/index.js +151 -0
  168. package/dist/src/languages/kotlin/parser.d.ts +11 -0
  169. package/dist/src/languages/kotlin/parser.d.ts.map +1 -0
  170. package/dist/src/languages/kotlin/parser.js +74 -0
  171. package/dist/src/languages/python/index.d.ts +15 -0
  172. package/dist/src/languages/python/index.d.ts.map +1 -0
  173. package/dist/src/languages/python/index.js +293 -0
  174. package/dist/src/languages/ruby/index.d.ts +15 -0
  175. package/dist/src/languages/ruby/index.d.ts.map +1 -0
  176. package/dist/src/languages/ruby/index.js +274 -0
  177. package/dist/src/languages/shared/treeSitterUtils.d.ts +43 -0
  178. package/dist/src/languages/shared/treeSitterUtils.d.ts.map +1 -0
  179. package/dist/src/languages/shared/treeSitterUtils.js +100 -0
  180. package/dist/src/languages/typescript/index.d.ts +14 -0
  181. package/dist/src/languages/typescript/index.d.ts.map +1 -0
  182. package/dist/src/languages/typescript/index.js +25 -0
  183. package/dist/src/lib/index.d.ts +228 -0
  184. package/dist/src/lib/index.d.ts.map +1 -0
  185. package/dist/src/lib/index.js +486 -0
  186. package/dist/src/mcp/client/index.d.ts +37 -0
  187. package/dist/src/mcp/client/index.d.ts.map +1 -0
  188. package/dist/src/mcp/client/index.js +235 -0
  189. package/dist/src/mcp/config.d.ts +50 -0
  190. package/dist/src/mcp/config.d.ts.map +1 -0
  191. package/dist/src/mcp/config.js +125 -0
  192. package/dist/src/mcp/events.d.ts +24 -0
  193. package/dist/src/mcp/events.d.ts.map +1 -0
  194. package/dist/src/mcp/events.js +48 -0
  195. package/dist/src/mcp/fallback/index.d.ts +50 -0
  196. package/dist/src/mcp/fallback/index.d.ts.map +1 -0
  197. package/dist/src/mcp/fallback/index.js +216 -0
  198. package/dist/src/mcp/index.d.ts +67 -0
  199. package/dist/src/mcp/index.d.ts.map +1 -0
  200. package/dist/src/mcp/index.js +212 -0
  201. package/dist/src/mcp/normalizer.d.ts +21 -0
  202. package/dist/src/mcp/normalizer.d.ts.map +1 -0
  203. package/dist/src/mcp/normalizer.js +99 -0
  204. package/dist/src/mcp/prompts/index.d.ts +86 -0
  205. package/dist/src/mcp/prompts/index.d.ts.map +1 -0
  206. package/dist/src/mcp/prompts/index.js +304 -0
  207. package/dist/src/mcp/templates/index.d.ts +35 -0
  208. package/dist/src/mcp/templates/index.d.ts.map +1 -0
  209. package/dist/src/mcp/templates/index.js +143 -0
  210. package/dist/src/mcp/testing/mock-server/index.d.ts +47 -0
  211. package/dist/src/mcp/testing/mock-server/index.d.ts.map +1 -0
  212. package/dist/src/mcp/testing/mock-server/index.js +157 -0
  213. package/dist/src/mcp/types.d.ts +127 -0
  214. package/dist/src/mcp/types.d.ts.map +1 -0
  215. package/dist/src/mcp/types.js +8 -0
  216. package/dist/src/observability.d.ts +138 -0
  217. package/dist/src/observability.d.ts.map +1 -0
  218. package/dist/src/observability.js +519 -0
  219. package/dist/src/parameterCoverage.d.ts +75 -0
  220. package/dist/src/parameterCoverage.d.ts.map +1 -0
  221. package/dist/src/parameterCoverage.js +629 -0
  222. package/dist/src/perfResilienceCoverage.d.ts +155 -0
  223. package/dist/src/perfResilienceCoverage.d.ts.map +1 -0
  224. package/dist/src/perfResilienceCoverage.js +670 -0
  225. package/dist/src/pluginLoader.d.ts +51 -0
  226. package/dist/src/pluginLoader.d.ts.map +1 -0
  227. package/dist/src/pluginLoader.js +72 -0
  228. package/dist/src/publishing.d.ts +63 -0
  229. package/dist/src/publishing.d.ts.map +1 -0
  230. package/dist/src/publishing.js +379 -0
  231. package/dist/src/qualityGate.d.ts +58 -0
  232. package/dist/src/qualityGate.d.ts.map +1 -0
  233. package/dist/src/qualityGate.js +118 -0
  234. package/dist/src/reporting.d.ts +41 -0
  235. package/dist/src/reporting.d.ts.map +1 -0
  236. package/dist/src/reporting.js +278 -0
  237. package/dist/src/screenshots.d.ts +71 -0
  238. package/dist/src/screenshots.d.ts.map +1 -0
  239. package/dist/src/screenshots.js +141 -0
  240. package/dist/src/security/gate/index.d.ts +11 -0
  241. package/dist/src/security/gate/index.d.ts.map +1 -0
  242. package/dist/src/security/gate/index.js +65 -0
  243. package/dist/src/security/index.d.ts +30 -0
  244. package/dist/src/security/index.d.ts.map +1 -0
  245. package/dist/src/security/index.js +342 -0
  246. package/dist/src/security/normalizers/semgrep.d.ts +10 -0
  247. package/dist/src/security/normalizers/semgrep.d.ts.map +1 -0
  248. package/dist/src/security/normalizers/semgrep.js +104 -0
  249. package/dist/src/security/normalizers/trivy.d.ts +10 -0
  250. package/dist/src/security/normalizers/trivy.d.ts.map +1 -0
  251. package/dist/src/security/normalizers/trivy.js +78 -0
  252. package/dist/src/security/normalizers/zap.d.ts +10 -0
  253. package/dist/src/security/normalizers/zap.d.ts.map +1 -0
  254. package/dist/src/security/normalizers/zap.js +104 -0
  255. package/dist/src/security/scanners/semgrep.d.ts +6 -0
  256. package/dist/src/security/scanners/semgrep.d.ts.map +1 -0
  257. package/dist/src/security/scanners/semgrep.js +125 -0
  258. package/dist/src/security/scanners/trivy.d.ts +6 -0
  259. package/dist/src/security/scanners/trivy.d.ts.map +1 -0
  260. package/dist/src/security/scanners/trivy.js +115 -0
  261. package/dist/src/security/scanners/zap.d.ts +6 -0
  262. package/dist/src/security/scanners/zap.d.ts.map +1 -0
  263. package/dist/src/security/scanners/zap.js +135 -0
  264. package/dist/src/security/types.d.ts +146 -0
  265. package/dist/src/security/types.d.ts.map +1 -0
  266. package/dist/src/security/types.js +6 -0
  267. package/dist/src/securityCoverage.d.ts +116 -0
  268. package/dist/src/securityCoverage.d.ts.map +1 -0
  269. package/dist/src/securityCoverage.js +725 -0
  270. package/dist/src/summary/buildSummary.d.ts +28 -0
  271. package/dist/src/summary/buildSummary.d.ts.map +1 -0
  272. package/dist/src/summary/buildSummary.js +257 -0
  273. package/dist/src/summary/evaluateMetrics.d.ts +31 -0
  274. package/dist/src/summary/evaluateMetrics.d.ts.map +1 -0
  275. package/dist/src/summary/evaluateMetrics.js +118 -0
  276. package/dist/src/summary/index.d.ts +10 -0
  277. package/dist/src/summary/index.d.ts.map +1 -0
  278. package/dist/src/summary/index.js +22 -0
  279. package/dist/src/summary/markdownRenderer.d.ts +139 -0
  280. package/dist/src/summary/markdownRenderer.d.ts.map +1 -0
  281. package/dist/src/summary/markdownRenderer.js +459 -0
  282. package/dist/src/summary/prSummary.d.ts +24 -0
  283. package/dist/src/summary/prSummary.d.ts.map +1 -0
  284. package/dist/src/summary/prSummary.js +233 -0
  285. package/dist/src/summary/summaryTypes.d.ts +35 -0
  286. package/dist/src/summary/summaryTypes.d.ts.map +1 -0
  287. package/dist/src/summary/summaryTypes.js +27 -0
  288. package/package.json +84 -0
@@ -0,0 +1,725 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.SECURITY_KEYWORDS = exports.SECURITY_CATEGORIES = void 0;
40
+ exports.parseSecurityControls = parseSecurityControls;
41
+ exports.parseScanReport = parseScanReport;
42
+ exports.alertNameToCategory = alertNameToCategory;
43
+ exports.testCoversControl = testCoversControl;
44
+ exports.analyzeSecurityCoverage = analyzeSecurityCoverage;
45
+ exports.buildSecurityCoverageReport = buildSecurityCoverageReport;
46
+ exports.generateSecurityReports = generateSecurityReports;
47
+ const swagger_parser_1 = __importDefault(require("@apidevtools/swagger-parser"));
48
+ const fs = __importStar(require("fs"));
49
+ const path = __importStar(require("path"));
50
+ const fast_glob_1 = __importDefault(require("fast-glob"));
51
+ const astAnalysisOrchestrator_1 = require("./ast/astAnalysisOrchestrator");
52
+ exports.SECURITY_CATEGORIES = [
53
+ 'authentication',
54
+ 'authorization',
55
+ 'input-validation',
56
+ 'cryptography',
57
+ 'session-management',
58
+ ];
59
+ // ─── Keyword maps ─────────────────────────────────────────────────────────────
60
+ /** Keywords used to detect security tests in test descriptions */
61
+ exports.SECURITY_KEYWORDS = {
62
+ 'authentication': [
63
+ '401', 'unauthorized', 'unauthenticated', 'authentication',
64
+ 'bearer', 'jwt', 'api key', 'apikey', 'login', 'sign in',
65
+ 'credential', 'no token', 'missing token', 'invalid token', 'auth token',
66
+ ],
67
+ 'authorization': [
68
+ '403', 'forbidden', 'authorization', 'permission', 'access denied',
69
+ 'role', 'privilege', 'rbac', 'access control', 'not allowed',
70
+ ],
71
+ 'input-validation': [
72
+ '400', '422', 'invalid', 'validation', 'bad request', 'malformed',
73
+ 'boundary', 'required field', 'missing field', 'missing required',
74
+ 'special characters', 'sql injection', 'xss', 'injection',
75
+ 'overflow', 'sanitize', 'required name', 'required email',
76
+ ],
77
+ 'cryptography': [
78
+ 'https', 'ssl', 'tls', 'certificate', 'encrypt', 'secure connection',
79
+ 'cipher', 'hash', 'hmac', 'signing algorithm',
80
+ ],
81
+ 'session-management': [
82
+ 'session', 'cookie', 'logout', 'sign out', 'refresh token',
83
+ 'token expiry', 'expired token', 'session timeout', 'revoke', 'invalidate',
84
+ ],
85
+ };
86
+ /** ZAP / scanner alert name patterns → SecurityCategory mappings */
87
+ const SCANNER_ALERT_PATTERNS = [
88
+ { pattern: /authentication/i, category: 'authentication' },
89
+ { pattern: /csrf|anti.forgery/i, category: 'authentication' },
90
+ { pattern: /authorization|access.control/i, category: 'authorization' },
91
+ { pattern: /injection|xss|cross.site|parameter.tamper|path.traversal|directory.traversal/i, category: 'input-validation' },
92
+ { pattern: /ssl|tls|certificate|cipher|transport.security|https/i, category: 'cryptography' },
93
+ { pattern: /session|cookie|token.expir/i, category: 'session-management' },
94
+ ];
95
+ // ─── Parsing the OpenAPI spec ─────────────────────────────────────────────────
96
+ const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'];
97
+ /**
98
+ * Extract security controls from a validated OpenAPI 3.x document.
99
+ * Controls are derived from:
100
+ * - Security schemes (authentication / session-management)
101
+ * - Endpoint security requirements (authorization)
102
+ * - Parameter / request-body validation constraints (input-validation)
103
+ * - Server URLs (cryptography)
104
+ */
105
+ async function parseSecurityControls(specPath) {
106
+ var _a, _b, _c, _d;
107
+ const api = (await swagger_parser_1.default.validate(specPath));
108
+ const controls = [];
109
+ // ── 1. Authentication / session-management: one control per security scheme ─
110
+ const schemes = ((_b = (_a = api.components) === null || _a === void 0 ? void 0 : _a.securitySchemes) !== null && _b !== void 0 ? _b : {});
111
+ for (const [name, scheme] of Object.entries(schemes)) {
112
+ const isSession = scheme.type === 'apiKey' &&
113
+ scheme.in === 'cookie';
114
+ const category = isSession ? 'session-management' : 'authentication';
115
+ let detail = scheme.type;
116
+ if (scheme.type === 'http') {
117
+ detail = `http/${scheme.scheme}`;
118
+ }
119
+ else if (scheme.type === 'oauth2') {
120
+ detail = 'oauth2';
121
+ }
122
+ else if (scheme.type === 'openIdConnect') {
123
+ detail = 'openIdConnect';
124
+ }
125
+ else if (scheme.type === 'apiKey') {
126
+ const loc = scheme.in;
127
+ detail = `apiKey/${loc}`;
128
+ }
129
+ controls.push({
130
+ id: `${category}:${name}`,
131
+ category,
132
+ description: category === 'session-management'
133
+ ? `Session management via cookie-based security scheme "${name}"`
134
+ : `Authentication via security scheme "${name}" (${detail})`,
135
+ detail,
136
+ });
137
+ }
138
+ // ── 2. Cryptography: HTTPS server usage ──────────────────────────────────
139
+ if (api.servers && api.servers.length > 0) {
140
+ const allHttps = api.servers.every((s) => s.url.startsWith('https://') || s.url.startsWith('/'));
141
+ controls.push({
142
+ id: 'cryptography:https',
143
+ category: 'cryptography',
144
+ description: allHttps
145
+ ? 'API servers use HTTPS – verify TLS configuration in tests'
146
+ : 'API servers include non-HTTPS URLs – cryptographic transport security may be missing',
147
+ detail: allHttps ? 'https' : 'mixed',
148
+ });
149
+ }
150
+ else {
151
+ controls.push({
152
+ id: 'cryptography:transport',
153
+ category: 'cryptography',
154
+ description: 'Verify that the API enforces secure transport (HTTPS/TLS)',
155
+ detail: 'unspecified',
156
+ });
157
+ }
158
+ // ── 3. Authorization & input-validation: per endpoint ────────────────────
159
+ const globalSecurity = (_c = api.security) !== null && _c !== void 0 ? _c : [];
160
+ for (const [apiPath, pathItem] of Object.entries((_d = api.paths) !== null && _d !== void 0 ? _d : {})) {
161
+ if (!pathItem)
162
+ continue;
163
+ for (const method of HTTP_METHODS) {
164
+ const operation = pathItem[method];
165
+ if (!operation)
166
+ continue;
167
+ const endpoint = `${method.toUpperCase()} ${apiPath}`;
168
+ // Determine effective security: operation-level overrides global; empty array means no auth
169
+ const effectiveSecurity = operation.security !== undefined ? operation.security : globalSecurity;
170
+ // Authorization control: endpoint has security requirements
171
+ if (effectiveSecurity.length > 0) {
172
+ controls.push({
173
+ id: `authorization:${method}:${apiPath}`,
174
+ category: 'authorization',
175
+ description: `Authorization check for ${endpoint}`,
176
+ endpoint,
177
+ detail: effectiveSecurity
178
+ .flatMap((req) => Object.keys(req))
179
+ .join(', '),
180
+ });
181
+ }
182
+ // Input-validation control: endpoint has constrained parameters or request body
183
+ const hasValidatedParams = hasParameterValidation(operation, pathItem);
184
+ const hasValidatedBody = hasRequestBodyValidation(operation);
185
+ if (hasValidatedParams || hasValidatedBody) {
186
+ controls.push({
187
+ id: `input-validation:${method}:${apiPath}`,
188
+ category: 'input-validation',
189
+ description: `Input validation for ${endpoint}`,
190
+ endpoint,
191
+ detail: hasValidatedParams && hasValidatedBody
192
+ ? 'parameters+body'
193
+ : hasValidatedParams
194
+ ? 'parameters'
195
+ : 'body',
196
+ });
197
+ }
198
+ }
199
+ }
200
+ return controls;
201
+ }
202
+ function hasParameterValidation(operation, pathItem) {
203
+ var _a, _b;
204
+ const params = [
205
+ ...((_a = pathItem.parameters) !== null && _a !== void 0 ? _a : []),
206
+ ...((_b = operation.parameters) !== null && _b !== void 0 ? _b : []),
207
+ ];
208
+ return params.some((p) => {
209
+ if (!('schema' in p) || !p.schema)
210
+ return false;
211
+ if (p.required)
212
+ return true;
213
+ const s = p.schema;
214
+ return !!(s.enum ||
215
+ s.pattern ||
216
+ s.minLength !== undefined ||
217
+ s.maxLength !== undefined ||
218
+ s.minimum !== undefined ||
219
+ s.maximum !== undefined ||
220
+ s.format);
221
+ });
222
+ }
223
+ function hasRequestBodyValidation(operation) {
224
+ var _a;
225
+ if (!operation.requestBody)
226
+ return false;
227
+ const rb = operation.requestBody;
228
+ if (rb.required)
229
+ return true;
230
+ for (const mediaObj of Object.values((_a = rb.content) !== null && _a !== void 0 ? _a : {})) {
231
+ const schema = mediaObj.schema;
232
+ if (!schema)
233
+ continue;
234
+ if (schema.required && schema.required.length > 0)
235
+ return true;
236
+ if (schema.properties) {
237
+ for (const prop of Object.values(schema.properties)) {
238
+ const ps = prop;
239
+ if (ps.enum ||
240
+ ps.pattern ||
241
+ ps.format ||
242
+ ps.minLength !== undefined ||
243
+ ps.maxLength !== undefined ||
244
+ ps.minimum !== undefined ||
245
+ ps.maximum !== undefined) {
246
+ return true;
247
+ }
248
+ }
249
+ }
250
+ }
251
+ return false;
252
+ }
253
+ /**
254
+ * Parse an external security scanner report and return normalised findings.
255
+ *
256
+ * Supported formats:
257
+ * • ZAP JSON – `{ "site": [{ "alerts": [{ "alert": "...", "riskdesc": "..." }] }] }`
258
+ * • Generic – `{ "findings": [{ "name": "...", "severity": "..." }] }`
259
+ * • Array – `[{ "name": "...", "severity": "..." }]`
260
+ * • XML – basic regex extraction of alert/name elements
261
+ */
262
+ function parseScanReport(reportPath) {
263
+ const raw = fs.readFileSync(reportPath, 'utf-8');
264
+ const ext = path.extname(reportPath).toLowerCase();
265
+ if (ext === '.xml') {
266
+ return parseScanReportXml(raw);
267
+ }
268
+ let parsed;
269
+ try {
270
+ parsed = JSON.parse(raw);
271
+ }
272
+ catch {
273
+ // Try XML fallback for files without .xml extension
274
+ return parseScanReportXml(raw);
275
+ }
276
+ return normaliseScanReport(parsed);
277
+ }
278
+ function parseScanReportXml(xml) {
279
+ const findings = [];
280
+ // Extract content from <alert> or <name> tags (common in OWASP ZAP XML reports)
281
+ const tagPattern = /<(?:alert|name)>([\s\S]*?)<\/(?:alert|name)>/g;
282
+ let m;
283
+ while ((m = tagPattern.exec(xml)) !== null) {
284
+ const name = m[1].trim();
285
+ if (name) {
286
+ const category = alertNameToCategory(name);
287
+ if (category) {
288
+ findings.push({ name, category });
289
+ }
290
+ }
291
+ }
292
+ return findings;
293
+ }
294
+ function normaliseScanReport(parsed) {
295
+ var _a, _b, _c, _d, _e, _f, _g;
296
+ if (!parsed || typeof parsed !== 'object')
297
+ return [];
298
+ // ZAP JSON format: { "site": [{ "alerts": [...] }] }
299
+ if ('site' in parsed) {
300
+ const findings = [];
301
+ const sites = (_a = parsed.site) !== null && _a !== void 0 ? _a : [];
302
+ for (const site of sites) {
303
+ if (!site || typeof site !== 'object')
304
+ continue;
305
+ const alerts = (_b = site.alerts) !== null && _b !== void 0 ? _b : [];
306
+ for (const alert of alerts) {
307
+ if (!alert || typeof alert !== 'object')
308
+ continue;
309
+ const a = alert;
310
+ const name = String((_d = (_c = a.alert) !== null && _c !== void 0 ? _c : a.name) !== null && _d !== void 0 ? _d : '');
311
+ const severity = String((_f = (_e = a.riskdesc) !== null && _e !== void 0 ? _e : a.severity) !== null && _f !== void 0 ? _f : '');
312
+ const category = alertNameToCategory(name);
313
+ if (category) {
314
+ findings.push({ name, category, severity });
315
+ }
316
+ }
317
+ }
318
+ return findings;
319
+ }
320
+ // Generic { findings: [...] } format
321
+ if ('findings' in parsed) {
322
+ const items = (_g = parsed.findings) !== null && _g !== void 0 ? _g : [];
323
+ return extractFindingsFromArray(items);
324
+ }
325
+ // Plain array
326
+ if (Array.isArray(parsed)) {
327
+ return extractFindingsFromArray(parsed);
328
+ }
329
+ return [];
330
+ }
331
+ function extractFindingsFromArray(items) {
332
+ var _a, _b, _c, _d, _e, _f;
333
+ const findings = [];
334
+ for (const item of items) {
335
+ if (!item || typeof item !== 'object')
336
+ continue;
337
+ const i = item;
338
+ const name = String((_c = (_b = (_a = i.name) !== null && _a !== void 0 ? _a : i.alert) !== null && _b !== void 0 ? _b : i.title) !== null && _c !== void 0 ? _c : '');
339
+ const severity = String((_f = (_e = (_d = i.severity) !== null && _d !== void 0 ? _d : i.riskdesc) !== null && _e !== void 0 ? _e : i.risk) !== null && _f !== void 0 ? _f : '');
340
+ const endpoint = typeof i.endpoint === 'string'
341
+ ? i.endpoint
342
+ : typeof i.url === 'string'
343
+ ? i.url
344
+ : undefined;
345
+ const category = alertNameToCategory(name);
346
+ if (category) {
347
+ findings.push({ name, category, severity, endpoint });
348
+ }
349
+ }
350
+ return findings;
351
+ }
352
+ /**
353
+ * Map a scanner alert name to a SecurityCategory using known patterns.
354
+ * Returns null if the alert name does not map to any category.
355
+ */
356
+ function alertNameToCategory(name) {
357
+ for (const { pattern, category } of SCANNER_ALERT_PATTERNS) {
358
+ if (pattern.test(name))
359
+ return category;
360
+ }
361
+ return null;
362
+ }
363
+ function extractTestEntries(filePath, fileContents) {
364
+ const entries = [];
365
+ const declPattern = /\b(?:test|it)\s*\(\s*(['"`])([\s\S]*?)\1/g;
366
+ const positions = [];
367
+ let m;
368
+ while ((m = declPattern.exec(fileContents)) !== null) {
369
+ positions.push({ start: m.index, desc: m[2] });
370
+ }
371
+ for (let i = 0; i < positions.length; i++) {
372
+ const start = positions[i].start;
373
+ const end = i + 1 < positions.length ? positions[i + 1].start : fileContents.length;
374
+ entries.push({
375
+ description: positions[i].desc,
376
+ content: fileContents.slice(start, end),
377
+ filePath,
378
+ });
379
+ }
380
+ return entries;
381
+ }
382
+ /**
383
+ * Determine whether a test entry covers a given security control.
384
+ *
385
+ * Matching rules (in order of precedence):
386
+ * 1. `@security <controlId>` annotation in description or surrounding content
387
+ * 2. Category keywords present in the test description
388
+ * 3. For endpoint-specific controls, the test description or content must
389
+ * also reference the endpoint path (matched after stripping path params)
390
+ */
391
+ function testCoversControl(entry, control) {
392
+ // 1. Annotation-based match: @security authorization:get:/users
393
+ const annotationPattern = new RegExp(`@security\\s+${escapeRegex(control.id)}`, 'i');
394
+ if (annotationPattern.test(entry.description) || annotationPattern.test(entry.content)) {
395
+ return true;
396
+ }
397
+ // 2. Category keywords in description
398
+ const descLower = entry.description.toLowerCase();
399
+ const keywords = exports.SECURITY_KEYWORDS[control.category];
400
+ const hasKeyword = keywords.some((kw) => descLower.includes(kw.toLowerCase()));
401
+ if (!hasKeyword)
402
+ return false;
403
+ // 3. For endpoint-specific controls, also require the path to be mentioned
404
+ if (control.endpoint) {
405
+ const endpointPath = control.endpoint.split(' ')[1]; // e.g. "/users/{id}"
406
+ // Normalise path params: /users/{id} → /users/
407
+ const normalised = endpointPath.replace(/\{[^}]+\}/g, '');
408
+ const contentLower = entry.content.toLowerCase();
409
+ if (!descLower.includes(normalised.toLowerCase()) &&
410
+ !contentLower.includes(normalised.toLowerCase())) {
411
+ return false;
412
+ }
413
+ }
414
+ return true;
415
+ }
416
+ function escapeRegex(s) {
417
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
418
+ }
419
+ // ─── AST augmentation ─────────────────────────────────────────────────────────
420
+ /** Detect language from file extension for AST analysis. */
421
+ function detectLanguageForSec(filePath) {
422
+ const ext = path.extname(filePath).toLowerCase();
423
+ switch (ext) {
424
+ case '.ts':
425
+ case '.tsx': return 'typescript';
426
+ case '.js':
427
+ case '.jsx': return 'javascript';
428
+ case '.java': return 'java';
429
+ case '.kt':
430
+ case '.kts': return 'kotlin';
431
+ case '.py': return 'python';
432
+ case '.rb': return 'ruby';
433
+ case '.feature': return 'cucumber';
434
+ default: return 'auto';
435
+ }
436
+ }
437
+ /** Normalise path for loose matching. */
438
+ function normalizeSecPath(p) {
439
+ return p.split('?')[0].replace(/\/$/, '').toLowerCase();
440
+ }
441
+ /** Build an endpoint → interactions lookup from test files. */
442
+ function buildSecAstMap(testFiles, astOptions) {
443
+ var _a;
444
+ (0, astAnalysisOrchestrator_1.registerAllAnalyzers)();
445
+ const context = (0, astAnalysisOrchestrator_1.buildAnalysisContext)(astOptions.astConfig, astOptions.deepConfig);
446
+ const map = new Map();
447
+ for (const filePath of testFiles) {
448
+ let content;
449
+ try {
450
+ content = fs.readFileSync(filePath, 'utf-8');
451
+ }
452
+ catch {
453
+ continue;
454
+ }
455
+ const lang = detectLanguageForSec(filePath);
456
+ const interactions = (0, astAnalysisOrchestrator_1.analyzeFile)(content, filePath, lang, context);
457
+ for (const interaction of interactions) {
458
+ const key = normalizeSecPath((_a = interaction.normalizedPath) !== null && _a !== void 0 ? _a : interaction.path);
459
+ if (!map.has(key))
460
+ map.set(key, []);
461
+ map.get(key).push(interaction);
462
+ }
463
+ }
464
+ return map;
465
+ }
466
+ /**
467
+ * Map parameterScenarios / assertionType to SecurityCategory coverage signals.
468
+ * Returns the categories that the interaction hints at.
469
+ */
470
+ function interactionToSecCategories(interaction) {
471
+ var _a;
472
+ const cats = [];
473
+ const scenarios = ((_a = interaction.parameterScenarios) !== null && _a !== void 0 ? _a : []).map((s) => s.toLowerCase());
474
+ // Authentication signals
475
+ if (interaction.assertionType === 'status-code' ||
476
+ interaction.assertionType === 'fluent-chain' ||
477
+ scenarios.some((s) => s.includes('unauth') || s.includes('token') || s.includes('bearer') ||
478
+ s.includes('credential') || s.includes('login') || s.includes('401'))) {
479
+ cats.push('authentication');
480
+ }
481
+ // Authorization signals
482
+ if (scenarios.some((s) => s.includes('forbidden') || s.includes('permission') || s.includes('role') ||
483
+ s.includes('access') || s.includes('403'))) {
484
+ cats.push('authorization');
485
+ }
486
+ // Input-validation signals
487
+ if (scenarios.some((s) => s.includes('invalid') || s.includes('missing') || s.includes('malformed') ||
488
+ s.includes('injection') || s.includes('xss') || s.includes('400') || s.includes('422'))) {
489
+ cats.push('input-validation');
490
+ }
491
+ // Cryptography signals
492
+ if (scenarios.some((s) => s.includes('https') || s.includes('ssl') || s.includes('tls') || s.includes('encrypt'))) {
493
+ cats.push('cryptography');
494
+ }
495
+ // Session-management signals
496
+ if (scenarios.some((s) => s.includes('session') || s.includes('cookie') || s.includes('logout') ||
497
+ s.includes('refresh') || s.includes('expir'))) {
498
+ cats.push('session-management');
499
+ }
500
+ return cats;
501
+ }
502
+ /**
503
+ * Check if any AST interactions at a control's endpoint cover the control.
504
+ */
505
+ function checkAstSecurityCoverage(astMap, control) {
506
+ var _a, _b, _c;
507
+ // For endpoint-specific controls, find interactions at that path
508
+ const matchingInteractions = [];
509
+ for (const [, interactions] of astMap) {
510
+ for (const interaction of interactions) {
511
+ // Endpoint-specific controls: require path match
512
+ if (control.endpoint) {
513
+ const endpointPath = control.endpoint.split(' ')[1];
514
+ const normalised = endpointPath.replace(/\{[^}]+\}/g, '').toLowerCase();
515
+ const iPath = normalizeSecPath((_a = interaction.normalizedPath) !== null && _a !== void 0 ? _a : interaction.path);
516
+ if (!iPath.startsWith(normalised))
517
+ continue;
518
+ const method = control.endpoint.split(' ')[0].toUpperCase();
519
+ if (interaction.method.toUpperCase() !== method)
520
+ continue;
521
+ }
522
+ const coveredCats = interactionToSecCategories(interaction);
523
+ if (coveredCats.includes(control.category)) {
524
+ matchingInteractions.push(interaction);
525
+ }
526
+ }
527
+ }
528
+ if (matchingInteractions.length === 0)
529
+ return { covered: false };
530
+ const best = (_c = (_b = matchingInteractions.find((i) => i.confidence === 'high')) !== null && _b !== void 0 ? _b : matchingInteractions.find((i) => i.confidence === 'medium')) !== null && _c !== void 0 ? _c : matchingInteractions[0];
531
+ return {
532
+ covered: true,
533
+ astMetadata: {
534
+ sourceLanguage: best.sourceLanguage,
535
+ resolutionType: best.resolutionType,
536
+ confidence: best.confidence,
537
+ },
538
+ };
539
+ }
540
+ // ─── Main analysis ────────────────────────────────────────────────────────────
541
+ /**
542
+ * Analyse test files (and optionally an external scan report) to determine
543
+ * which security controls from the spec are covered. Optionally augments
544
+ * text-scan results with AST-derived semantic signals when `astOptions` is
545
+ * provided.
546
+ */
547
+ async function analyzeSecurityCoverage(controls, testGlob, scanReportPath, astOptions) {
548
+ // Load and parse test files
549
+ const testFiles = await (0, fast_glob_1.default)(testGlob, { onlyFiles: true });
550
+ const allEntries = [];
551
+ for (const filePath of testFiles) {
552
+ const contents = fs.readFileSync(filePath, 'utf-8');
553
+ allEntries.push(...extractTestEntries(filePath, contents));
554
+ }
555
+ // Load scan report findings
556
+ const scanFindings = scanReportPath ? parseScanReport(scanReportPath) : [];
557
+ // Build AST map when options provided
558
+ const astMap = astOptions
559
+ ? buildSecAstMap(testFiles, astOptions)
560
+ : null;
561
+ return controls.map((control) => {
562
+ // ── Text-scan pass ──────────────────────────────────────────────────────
563
+ const matchedTests = [];
564
+ for (const entry of allEntries) {
565
+ if (testCoversControl(entry, control)) {
566
+ if (!matchedTests.includes(entry.description)) {
567
+ matchedTests.push(entry.description);
568
+ }
569
+ }
570
+ }
571
+ // Scan-report-based coverage
572
+ const coveredByScanReport = scanFindings.some((f) => f.category === control.category);
573
+ let covered = matchedTests.length > 0 || coveredByScanReport;
574
+ let astMetadata;
575
+ // ── AST augmentation pass ───────────────────────────────────────────────
576
+ if (astMap !== null) {
577
+ const astResult = checkAstSecurityCoverage(astMap, control);
578
+ if (astResult.covered) {
579
+ covered = true;
580
+ astMetadata = astResult.astMetadata;
581
+ }
582
+ }
583
+ return { control, covered, matchedTests, coveredByScanReport, astMetadata };
584
+ });
585
+ }
586
+ // ─── Report building ──────────────────────────────────────────────────────────
587
+ /**
588
+ * Build the coverage summary from per-control coverages.
589
+ */
590
+ function buildSecurityCoverageReport(coverages, scanFindings = 0) {
591
+ const total = coverages.length;
592
+ const coveredCount = coverages.filter((c) => c.covered).length;
593
+ const percentage = total === 0 ? 0 : Math.round((coveredCount / total) * 10000) / 100;
594
+ const categorySummary = {};
595
+ for (const cat of exports.SECURITY_CATEGORIES) {
596
+ const catControls = coverages.filter((c) => c.control.category === cat);
597
+ categorySummary[cat] = {
598
+ total: catControls.length,
599
+ covered: catControls.filter((c) => c.covered).length,
600
+ };
601
+ }
602
+ return {
603
+ total,
604
+ covered: coveredCount,
605
+ percentage,
606
+ controls: coverages,
607
+ categorySummary,
608
+ scanFindings,
609
+ };
610
+ }
611
+ // ─── Report generation ────────────────────────────────────────────────────────
612
+ /**
613
+ * Write JSON and HTML security-coverage reports to the given directory.
614
+ */
615
+ function generateSecurityReports(report, reportsDir) {
616
+ if (!fs.existsSync(reportsDir)) {
617
+ fs.mkdirSync(reportsDir, { recursive: true });
618
+ }
619
+ // ── JSON ────────────────────────────────────────────────────────────────
620
+ const jsonPath = path.join(reportsDir, 'security-coverage.json');
621
+ const jsonReport = {
622
+ total: report.total,
623
+ covered: report.covered,
624
+ percentage: report.percentage,
625
+ scanFindings: report.scanFindings,
626
+ categorySummary: report.categorySummary,
627
+ controls: report.controls.map(({ control, covered, matchedTests, coveredByScanReport }) => ({
628
+ id: control.id,
629
+ category: control.category,
630
+ description: control.description,
631
+ endpoint: control.endpoint,
632
+ covered,
633
+ matchedTests,
634
+ coveredByScanReport,
635
+ })),
636
+ };
637
+ fs.writeFileSync(jsonPath, JSON.stringify(jsonReport, null, 2), 'utf-8');
638
+ // ── HTML ────────────────────────────────────────────────────────────────
639
+ const htmlPath = path.join(reportsDir, 'security-coverage.html');
640
+ const catRows = exports.SECURITY_CATEGORIES.map((cat) => {
641
+ const s = report.categorySummary[cat];
642
+ const pct = s.total === 0 ? 100 : Math.round((s.covered / s.total) * 100);
643
+ const rowClass = pct >= 80 ? 'good' : pct >= 50 ? 'warn' : 'bad';
644
+ return ` <tr class="${rowClass}">
645
+ <td>${cat}</td>
646
+ <td>${s.covered}/${s.total}</td>
647
+ <td>${pct}%</td>
648
+ </tr>`;
649
+ }).join('\n');
650
+ const controlRows = report.controls
651
+ .map(({ control, covered, matchedTests, coveredByScanReport }) => {
652
+ var _a;
653
+ const rowClass = covered ? 'covered' : 'uncovered';
654
+ const status = covered
655
+ ? coveredByScanReport && matchedTests.length === 0
656
+ ? '🔍 Scanner only'
657
+ : '✅ Covered'
658
+ : '❌ Not covered';
659
+ const tests = matchedTests.length > 0 ? matchedTests.join('<br>') : '—';
660
+ const endpoint = (_a = control.endpoint) !== null && _a !== void 0 ? _a : '—';
661
+ return ` <tr class="${rowClass}">
662
+ <td>${control.id}</td>
663
+ <td>${control.category}</td>
664
+ <td>${control.description}</td>
665
+ <td>${endpoint}</td>
666
+ <td>${status}</td>
667
+ <td>${tests}</td>
668
+ </tr>`;
669
+ })
670
+ .join('\n');
671
+ const html = `<!DOCTYPE html>
672
+ <html lang="en">
673
+ <head>
674
+ <meta charset="UTF-8">
675
+ <title>Security Coverage Report</title>
676
+ <style>
677
+ body { font-family: sans-serif; padding: 2rem; }
678
+ h1, h2 { margin-bottom: 0.5rem; }
679
+ .summary { margin-bottom: 1.5rem; font-size: 1.1rem; }
680
+ table { border-collapse: collapse; width: 100%; margin-bottom: 2rem; }
681
+ th, td { border: 1px solid #ccc; padding: 0.5rem 1rem; text-align: left; }
682
+ th { background: #f0f0f0; }
683
+ tr.good { background: #e6ffe6; }
684
+ tr.warn { background: #fff9e6; }
685
+ tr.bad { background: #ffe6e6; }
686
+ tr.covered { background: #e6ffe6; }
687
+ tr.uncovered { background: #ffe6e6; }
688
+ </style>
689
+ </head>
690
+ <body>
691
+ <h1>Security Coverage Report</h1>
692
+ <div class="summary">
693
+ Covered: <strong>${report.covered}/${report.total}</strong> security controls
694
+ (<strong>${report.percentage}%</strong>)
695
+ ${report.scanFindings > 0 ? `&nbsp;|&nbsp; External scan findings: <strong>${report.scanFindings}</strong>` : ''}
696
+ </div>
697
+ <h2>By Category</h2>
698
+ <table>
699
+ <thead>
700
+ <tr><th>Category</th><th>Covered / Total</th><th>%</th></tr>
701
+ </thead>
702
+ <tbody>
703
+ ${catRows}
704
+ </tbody>
705
+ </table>
706
+ <h2>Controls Detail</h2>
707
+ <table>
708
+ <thead>
709
+ <tr>
710
+ <th>Control ID</th>
711
+ <th>Category</th>
712
+ <th>Description</th>
713
+ <th>Endpoint</th>
714
+ <th>Status</th>
715
+ <th>Matched Tests</th>
716
+ </tr>
717
+ </thead>
718
+ <tbody>
719
+ ${controlRows}
720
+ </tbody>
721
+ </table>
722
+ </body>
723
+ </html>`;
724
+ fs.writeFileSync(htmlPath, html, 'utf-8');
725
+ }