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.
- package/README.md +703 -0
- package/config.yaml.example +227 -0
- package/dist/action/src/index.d.ts +2 -0
- package/dist/action/src/index.d.ts.map +1 -0
- package/dist/action/src/index.js +349 -0
- package/dist/action/src/prComment.d.ts +34 -0
- package/dist/action/src/prComment.d.ts.map +1 -0
- package/dist/action/src/prComment.js +146 -0
- package/dist/src/ast/astAnalysisOrchestrator.d.ts +36 -0
- package/dist/src/ast/astAnalysisOrchestrator.d.ts.map +1 -0
- package/dist/src/ast/astAnalysisOrchestrator.js +123 -0
- package/dist/src/ast/astTypes.d.ts +105 -0
- package/dist/src/ast/astTypes.d.ts.map +1 -0
- package/dist/src/ast/astTypes.js +9 -0
- package/dist/src/ast/languageAnalyzer.d.ts +46 -0
- package/dist/src/ast/languageAnalyzer.d.ts.map +1 -0
- package/dist/src/ast/languageAnalyzer.js +9 -0
- package/dist/src/ast/languageCapabilities.d.ts +24 -0
- package/dist/src/ast/languageCapabilities.d.ts.map +1 -0
- package/dist/src/ast/languageCapabilities.js +92 -0
- package/dist/src/ast/parseFile.d.ts +16 -0
- package/dist/src/ast/parseFile.d.ts.map +1 -0
- package/dist/src/ast/parseFile.js +65 -0
- package/dist/src/ast/parserRegistry.d.ts +39 -0
- package/dist/src/ast/parserRegistry.d.ts.map +1 -0
- package/dist/src/ast/parserRegistry.js +66 -0
- package/dist/src/buildSummary.d.ts +26 -0
- package/dist/src/buildSummary.d.ts.map +1 -0
- package/dist/src/buildSummary.js +193 -0
- package/dist/src/businessCoverage.d.ts +68 -0
- package/dist/src/businessCoverage.d.ts.map +1 -0
- package/dist/src/businessCoverage.js +290 -0
- package/dist/src/compatibilityCoverage.d.ts +83 -0
- package/dist/src/compatibilityCoverage.d.ts.map +1 -0
- package/dist/src/compatibilityCoverage.js +501 -0
- package/dist/src/config/defaultConfig.d.ts +9 -0
- package/dist/src/config/defaultConfig.d.ts.map +1 -0
- package/dist/src/config/defaultConfig.js +97 -0
- package/dist/src/config/index.d.ts +12 -0
- package/dist/src/config/index.d.ts.map +1 -0
- package/dist/src/config/index.js +37 -0
- package/dist/src/config/loadConfig.d.ts +29 -0
- package/dist/src/config/loadConfig.d.ts.map +1 -0
- package/dist/src/config/loadConfig.js +135 -0
- package/dist/src/config/mergeConfig.d.ts +15 -0
- package/dist/src/config/mergeConfig.d.ts.map +1 -0
- package/dist/src/config/mergeConfig.js +57 -0
- package/dist/src/config/schema.d.ts +15 -0
- package/dist/src/config/schema.d.ts.map +1 -0
- package/dist/src/config/schema.js +30 -0
- package/dist/src/config/types.d.ts +175 -0
- package/dist/src/config/types.d.ts.map +1 -0
- package/dist/src/config/types.js +9 -0
- package/dist/src/config/validateConfig.d.ts +22 -0
- package/dist/src/config/validateConfig.d.ts.map +1 -0
- package/dist/src/config/validateConfig.js +171 -0
- package/dist/src/config.d.ts +168 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +204 -0
- package/dist/src/coverage/deep-analysis/callGraph.d.ts +67 -0
- package/dist/src/coverage/deep-analysis/callGraph.d.ts.map +1 -0
- package/dist/src/coverage/deep-analysis/callGraph.js +275 -0
- package/dist/src/coverage/deep-analysis/deepEndpointResolver.d.ts +23 -0
- package/dist/src/coverage/deep-analysis/deepEndpointResolver.d.ts.map +1 -0
- package/dist/src/coverage/deep-analysis/deepEndpointResolver.js +394 -0
- package/dist/src/coverage/deep-analysis/index.d.ts +17 -0
- package/dist/src/coverage/deep-analysis/index.d.ts.map +1 -0
- package/dist/src/coverage/deep-analysis/index.js +63 -0
- package/dist/src/coverage/deep-analysis/resolveAssertions.d.ts +60 -0
- package/dist/src/coverage/deep-analysis/resolveAssertions.d.ts.map +1 -0
- package/dist/src/coverage/deep-analysis/resolveAssertions.js +121 -0
- package/dist/src/coverage/deep-analysis/resolveConstants.d.ts +36 -0
- package/dist/src/coverage/deep-analysis/resolveConstants.d.ts.map +1 -0
- package/dist/src/coverage/deep-analysis/resolveConstants.js +92 -0
- package/dist/src/coverage/deep-analysis/resolveEnums.d.ts +55 -0
- package/dist/src/coverage/deep-analysis/resolveEnums.d.ts.map +1 -0
- package/dist/src/coverage/deep-analysis/resolveEnums.js +152 -0
- package/dist/src/coverage/deep-analysis/resolveMethodChains.d.ts +70 -0
- package/dist/src/coverage/deep-analysis/resolveMethodChains.d.ts.map +1 -0
- package/dist/src/coverage/deep-analysis/resolveMethodChains.js +152 -0
- package/dist/src/coverage/deep-analysis/resolvePaths.d.ts +80 -0
- package/dist/src/coverage/deep-analysis/resolvePaths.d.ts.map +1 -0
- package/dist/src/coverage/deep-analysis/resolvePaths.js +216 -0
- package/dist/src/coverage/deep-analysis/resolveRequestWrappers.d.ts +71 -0
- package/dist/src/coverage/deep-analysis/resolveRequestWrappers.d.ts.map +1 -0
- package/dist/src/coverage/deep-analysis/resolveRequestWrappers.js +226 -0
- package/dist/src/coverage/deep-analysis/symbolTable.d.ts +58 -0
- package/dist/src/coverage/deep-analysis/symbolTable.d.ts.map +1 -0
- package/dist/src/coverage/deep-analysis/symbolTable.js +230 -0
- package/dist/src/coverage/deep-analysis/types.d.ts +122 -0
- package/dist/src/coverage/deep-analysis/types.d.ts.map +1 -0
- package/dist/src/coverage/deep-analysis/types.js +21 -0
- package/dist/src/discovery/fileClassifier.d.ts +50 -0
- package/dist/src/discovery/fileClassifier.d.ts.map +1 -0
- package/dist/src/discovery/fileClassifier.js +238 -0
- package/dist/src/discovery/projectDiscovery.d.ts +66 -0
- package/dist/src/discovery/projectDiscovery.d.ts.map +1 -0
- package/dist/src/discovery/projectDiscovery.js +287 -0
- package/dist/src/endpointCoverage.d.ts +70 -0
- package/dist/src/endpointCoverage.d.ts.map +1 -0
- package/dist/src/endpointCoverage.js +381 -0
- package/dist/src/errorCoverage.d.ts +93 -0
- package/dist/src/errorCoverage.d.ts.map +1 -0
- package/dist/src/errorCoverage.js +698 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +1441 -0
- package/dist/src/inference/businessRuleInference.d.ts +63 -0
- package/dist/src/inference/businessRuleInference.d.ts.map +1 -0
- package/dist/src/inference/businessRuleInference.js +268 -0
- package/dist/src/inference/integrationFlowInference.d.ts +56 -0
- package/dist/src/inference/integrationFlowInference.d.ts.map +1 -0
- package/dist/src/inference/integrationFlowInference.js +266 -0
- package/dist/src/integrationCoverage.d.ts +72 -0
- package/dist/src/integrationCoverage.d.ts.map +1 -0
- package/dist/src/integrationCoverage.js +317 -0
- package/dist/src/intelligence/index.d.ts +20 -0
- package/dist/src/intelligence/index.d.ts.map +1 -0
- package/dist/src/intelligence/index.js +105 -0
- package/dist/src/intelligence/linkageEngine.d.ts +20 -0
- package/dist/src/intelligence/linkageEngine.d.ts.map +1 -0
- package/dist/src/intelligence/linkageEngine.js +522 -0
- package/dist/src/intelligence/markdownReporter.d.ts +12 -0
- package/dist/src/intelligence/markdownReporter.d.ts.map +1 -0
- package/dist/src/intelligence/markdownReporter.js +265 -0
- package/dist/src/intelligence/riskScoring.d.ts +53 -0
- package/dist/src/intelligence/riskScoring.d.ts.map +1 -0
- package/dist/src/intelligence/riskScoring.js +181 -0
- package/dist/src/intelligence/types.d.ts +121 -0
- package/dist/src/intelligence/types.d.ts.map +1 -0
- package/dist/src/intelligence/types.js +8 -0
- package/dist/src/languageDetection.d.ts +100 -0
- package/dist/src/languageDetection.d.ts.map +1 -0
- package/dist/src/languageDetection.js +349 -0
- package/dist/src/languages/java/index.d.ts +16 -0
- package/dist/src/languages/java/index.d.ts.map +1 -0
- package/dist/src/languages/java/index.js +103 -0
- package/dist/src/languages/java/parser.d.ts +7 -0
- package/dist/src/languages/java/parser.d.ts.map +1 -0
- package/dist/src/languages/java/parser.js +50 -0
- package/dist/src/languages/java/semanticBuilder.d.ts +21 -0
- package/dist/src/languages/java/semanticBuilder.d.ts.map +1 -0
- package/dist/src/languages/java/semanticBuilder.js +358 -0
- package/dist/src/languages/javascript/annotationExtractor.d.ts +20 -0
- package/dist/src/languages/javascript/annotationExtractor.d.ts.map +1 -0
- package/dist/src/languages/javascript/annotationExtractor.js +94 -0
- package/dist/src/languages/javascript/assertionResolver.d.ts +18 -0
- package/dist/src/languages/javascript/assertionResolver.d.ts.map +1 -0
- package/dist/src/languages/javascript/assertionResolver.js +150 -0
- package/dist/src/languages/javascript/callResolver.d.ts +23 -0
- package/dist/src/languages/javascript/callResolver.d.ts.map +1 -0
- package/dist/src/languages/javascript/callResolver.js +236 -0
- package/dist/src/languages/javascript/httpInteractionExtractor.d.ts +23 -0
- package/dist/src/languages/javascript/httpInteractionExtractor.d.ts.map +1 -0
- package/dist/src/languages/javascript/httpInteractionExtractor.js +205 -0
- package/dist/src/languages/javascript/index.d.ts +20 -0
- package/dist/src/languages/javascript/index.d.ts.map +1 -0
- package/dist/src/languages/javascript/index.js +136 -0
- package/dist/src/languages/javascript/parser.d.ts +14 -0
- package/dist/src/languages/javascript/parser.d.ts.map +1 -0
- package/dist/src/languages/javascript/parser.js +38 -0
- package/dist/src/languages/javascript/symbolResolver.d.ts +31 -0
- package/dist/src/languages/javascript/symbolResolver.d.ts.map +1 -0
- package/dist/src/languages/javascript/symbolResolver.js +183 -0
- package/dist/src/languages/kotlin/index.d.ts +16 -0
- package/dist/src/languages/kotlin/index.d.ts.map +1 -0
- package/dist/src/languages/kotlin/index.js +151 -0
- package/dist/src/languages/kotlin/parser.d.ts +11 -0
- package/dist/src/languages/kotlin/parser.d.ts.map +1 -0
- package/dist/src/languages/kotlin/parser.js +74 -0
- package/dist/src/languages/python/index.d.ts +15 -0
- package/dist/src/languages/python/index.d.ts.map +1 -0
- package/dist/src/languages/python/index.js +293 -0
- package/dist/src/languages/ruby/index.d.ts +15 -0
- package/dist/src/languages/ruby/index.d.ts.map +1 -0
- package/dist/src/languages/ruby/index.js +274 -0
- package/dist/src/languages/shared/treeSitterUtils.d.ts +43 -0
- package/dist/src/languages/shared/treeSitterUtils.d.ts.map +1 -0
- package/dist/src/languages/shared/treeSitterUtils.js +100 -0
- package/dist/src/languages/typescript/index.d.ts +14 -0
- package/dist/src/languages/typescript/index.d.ts.map +1 -0
- package/dist/src/languages/typescript/index.js +25 -0
- package/dist/src/lib/index.d.ts +228 -0
- package/dist/src/lib/index.d.ts.map +1 -0
- package/dist/src/lib/index.js +486 -0
- package/dist/src/mcp/client/index.d.ts +37 -0
- package/dist/src/mcp/client/index.d.ts.map +1 -0
- package/dist/src/mcp/client/index.js +235 -0
- package/dist/src/mcp/config.d.ts +50 -0
- package/dist/src/mcp/config.d.ts.map +1 -0
- package/dist/src/mcp/config.js +125 -0
- package/dist/src/mcp/events.d.ts +24 -0
- package/dist/src/mcp/events.d.ts.map +1 -0
- package/dist/src/mcp/events.js +48 -0
- package/dist/src/mcp/fallback/index.d.ts +50 -0
- package/dist/src/mcp/fallback/index.d.ts.map +1 -0
- package/dist/src/mcp/fallback/index.js +216 -0
- package/dist/src/mcp/index.d.ts +67 -0
- package/dist/src/mcp/index.d.ts.map +1 -0
- package/dist/src/mcp/index.js +212 -0
- package/dist/src/mcp/normalizer.d.ts +21 -0
- package/dist/src/mcp/normalizer.d.ts.map +1 -0
- package/dist/src/mcp/normalizer.js +99 -0
- package/dist/src/mcp/prompts/index.d.ts +86 -0
- package/dist/src/mcp/prompts/index.d.ts.map +1 -0
- package/dist/src/mcp/prompts/index.js +304 -0
- package/dist/src/mcp/templates/index.d.ts +35 -0
- package/dist/src/mcp/templates/index.d.ts.map +1 -0
- package/dist/src/mcp/templates/index.js +143 -0
- package/dist/src/mcp/testing/mock-server/index.d.ts +47 -0
- package/dist/src/mcp/testing/mock-server/index.d.ts.map +1 -0
- package/dist/src/mcp/testing/mock-server/index.js +157 -0
- package/dist/src/mcp/types.d.ts +127 -0
- package/dist/src/mcp/types.d.ts.map +1 -0
- package/dist/src/mcp/types.js +8 -0
- package/dist/src/observability.d.ts +138 -0
- package/dist/src/observability.d.ts.map +1 -0
- package/dist/src/observability.js +519 -0
- package/dist/src/parameterCoverage.d.ts +75 -0
- package/dist/src/parameterCoverage.d.ts.map +1 -0
- package/dist/src/parameterCoverage.js +629 -0
- package/dist/src/perfResilienceCoverage.d.ts +155 -0
- package/dist/src/perfResilienceCoverage.d.ts.map +1 -0
- package/dist/src/perfResilienceCoverage.js +670 -0
- package/dist/src/pluginLoader.d.ts +51 -0
- package/dist/src/pluginLoader.d.ts.map +1 -0
- package/dist/src/pluginLoader.js +72 -0
- package/dist/src/publishing.d.ts +63 -0
- package/dist/src/publishing.d.ts.map +1 -0
- package/dist/src/publishing.js +379 -0
- package/dist/src/qualityGate.d.ts +58 -0
- package/dist/src/qualityGate.d.ts.map +1 -0
- package/dist/src/qualityGate.js +118 -0
- package/dist/src/reporting.d.ts +41 -0
- package/dist/src/reporting.d.ts.map +1 -0
- package/dist/src/reporting.js +278 -0
- package/dist/src/screenshots.d.ts +71 -0
- package/dist/src/screenshots.d.ts.map +1 -0
- package/dist/src/screenshots.js +141 -0
- package/dist/src/security/gate/index.d.ts +11 -0
- package/dist/src/security/gate/index.d.ts.map +1 -0
- package/dist/src/security/gate/index.js +65 -0
- package/dist/src/security/index.d.ts +30 -0
- package/dist/src/security/index.d.ts.map +1 -0
- package/dist/src/security/index.js +342 -0
- package/dist/src/security/normalizers/semgrep.d.ts +10 -0
- package/dist/src/security/normalizers/semgrep.d.ts.map +1 -0
- package/dist/src/security/normalizers/semgrep.js +104 -0
- package/dist/src/security/normalizers/trivy.d.ts +10 -0
- package/dist/src/security/normalizers/trivy.d.ts.map +1 -0
- package/dist/src/security/normalizers/trivy.js +78 -0
- package/dist/src/security/normalizers/zap.d.ts +10 -0
- package/dist/src/security/normalizers/zap.d.ts.map +1 -0
- package/dist/src/security/normalizers/zap.js +104 -0
- package/dist/src/security/scanners/semgrep.d.ts +6 -0
- package/dist/src/security/scanners/semgrep.d.ts.map +1 -0
- package/dist/src/security/scanners/semgrep.js +125 -0
- package/dist/src/security/scanners/trivy.d.ts +6 -0
- package/dist/src/security/scanners/trivy.d.ts.map +1 -0
- package/dist/src/security/scanners/trivy.js +115 -0
- package/dist/src/security/scanners/zap.d.ts +6 -0
- package/dist/src/security/scanners/zap.d.ts.map +1 -0
- package/dist/src/security/scanners/zap.js +135 -0
- package/dist/src/security/types.d.ts +146 -0
- package/dist/src/security/types.d.ts.map +1 -0
- package/dist/src/security/types.js +6 -0
- package/dist/src/securityCoverage.d.ts +116 -0
- package/dist/src/securityCoverage.d.ts.map +1 -0
- package/dist/src/securityCoverage.js +725 -0
- package/dist/src/summary/buildSummary.d.ts +28 -0
- package/dist/src/summary/buildSummary.d.ts.map +1 -0
- package/dist/src/summary/buildSummary.js +257 -0
- package/dist/src/summary/evaluateMetrics.d.ts +31 -0
- package/dist/src/summary/evaluateMetrics.d.ts.map +1 -0
- package/dist/src/summary/evaluateMetrics.js +118 -0
- package/dist/src/summary/index.d.ts +10 -0
- package/dist/src/summary/index.d.ts.map +1 -0
- package/dist/src/summary/index.js +22 -0
- package/dist/src/summary/markdownRenderer.d.ts +139 -0
- package/dist/src/summary/markdownRenderer.d.ts.map +1 -0
- package/dist/src/summary/markdownRenderer.js +459 -0
- package/dist/src/summary/prSummary.d.ts +24 -0
- package/dist/src/summary/prSummary.d.ts.map +1 -0
- package/dist/src/summary/prSummary.js +233 -0
- package/dist/src/summary/summaryTypes.d.ts +35 -0
- package/dist/src/summary/summaryTypes.d.ts.map +1 -0
- package/dist/src/summary/summaryTypes.js +27 -0
- 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 ? ` | 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
|
+
}
|