Skip to content

Commit 64136dd

Browse files
Merge pull request #39 from GovindarajanL/framework-reporting
Framework reporting
2 parents 0461d76 + d3dfbdd commit 64136dd

File tree

12 files changed

+2682
-169
lines changed

12 files changed

+2682
-169
lines changed

pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,18 @@
9696
<version>${junit.version}</version>
9797
<scope>test</scope>
9898
</dependency>
99+
<!-- YAML Processing -->
100+
<dependency>
101+
<groupId>com.fasterxml.jackson.dataformat</groupId>
102+
<artifactId>jackson-dataformat-yaml</artifactId>
103+
<version>${jackson.version}</version>
104+
</dependency>
105+
<!-- JSON Processing -->
106+
<dependency>
107+
<groupId>com.fasterxml.jackson.core</groupId>
108+
<artifactId>jackson-databind</artifactId>
109+
<version>${jackson.version}</version>
110+
</dependency>
99111
</dependencies>
100112

101113
<build>
Lines changed: 144 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package org.owasp.astf.core;
22

3+
import java.time.LocalDateTime;
34
import java.util.ArrayList;
45
import java.util.List;
56
import java.util.Map;
7+
import java.util.concurrent.CompletableFuture;
8+
import java.util.concurrent.ConcurrentHashMap;
69
import java.util.concurrent.ExecutorService;
710
import java.util.concurrent.Executors;
811
import java.util.concurrent.TimeUnit;
12+
import java.util.concurrent.atomic.AtomicInteger;
913

1014
import org.apache.logging.log4j.LogManager;
1115
import org.apache.logging.log4j.Logger;
1216
import org.owasp.astf.core.config.ScanConfig;
17+
import org.owasp.astf.core.discovery.EndpointDiscoveryService;
1318
import org.owasp.astf.core.http.HttpClient;
1419
import org.owasp.astf.core.result.Finding;
1520
import org.owasp.astf.core.result.ScanResult;
@@ -19,126 +24,184 @@
1924

2025
/**
2126
* The main scanner engine that orchestrates the API security testing process.
27+
* This class is responsible for:
28+
* <ul>
29+
* <li>Initializing and executing the scan based on configuration</li>
30+
* <li>Managing endpoint discovery or using provided endpoints</li>
31+
* <li>Coordinating test case execution across endpoints</li>
32+
* <li>Collecting and aggregating findings</li>
33+
* <li>Providing progress updates and metrics</li>
34+
* </ul>
2235
*/
2336
public class Scanner {
2437
private static final Logger logger = LogManager.getLogger(Scanner.class);
2538

2639
private final ScanConfig config;
2740
private final HttpClient httpClient;
2841
private final TestCaseRegistry testCaseRegistry;
42+
private final EndpointDiscoveryService discoveryService;
2943

44+
// Scan metrics and tracking
45+
private final AtomicInteger completedTasks = new AtomicInteger(0);
46+
private final AtomicInteger totalTasks = new AtomicInteger(0);
47+
private final Map<Severity, AtomicInteger> findingsBySeverity = new ConcurrentHashMap<>();
48+
private LocalDateTime scanStartTime;
49+
private LocalDateTime scanEndTime;
50+
51+
/**
52+
* Creates a new scanner with the specified configuration.
53+
*
54+
* @param config The scan configuration
55+
*/
3056
public Scanner(ScanConfig config) {
3157
this.config = config;
3258
this.httpClient = new HttpClient(config);
3359
this.testCaseRegistry = new TestCaseRegistry();
60+
this.discoveryService = new EndpointDiscoveryService(config, httpClient);
61+
62+
// Initialize severity counters
63+
for (Severity severity : Severity.values()) {
64+
findingsBySeverity.put(severity, new AtomicInteger(0));
65+
}
3466
}
3567

3668
/**
3769
* Executes a full scan based on the provided configuration.
3870
*
39-
* @return The scan results containing all findings.
71+
* @return The scan results containing all findings
4072
*/
4173
public ScanResult scan() {
42-
logger.info("Starting API security scan for target: {}", config.getTargetUrl());
43-
74+
scanStartTime = LocalDateTime.now();
4475
List<Finding> findings = new ArrayList<>();
4576

46-
// Determine if we need to discover endpoints or use provided ones
47-
List<EndpointInfo> endpoints = new ArrayList<>();
48-
if (config.isDiscoveryEnabled() && config.getEndpoints().isEmpty()) {
49-
endpoints = discoverEndpoints();
50-
} else {
51-
endpoints = config.getEndpoints();
52-
}
77+
try {
78+
logger.info("Starting API security scan for target: {}", config.getTargetUrl());
79+
80+
// Determine if we need to discover endpoints or use provided ones
81+
List<EndpointInfo> endpoints = new ArrayList<>();
82+
if (config.isDiscoveryEnabled() && config.getEndpoints().isEmpty()) {
83+
logger.info("No endpoints provided. Attempting endpoint discovery...");
84+
endpoints = discoverEndpoints();
85+
} else {
86+
endpoints = config.getEndpoints();
87+
logger.info("Using {} provided endpoints", endpoints.size());
88+
}
89+
90+
if (endpoints.isEmpty()) {
91+
logger.warn("No endpoints found to scan. Check target URL or provide endpoints manually.");
92+
return createEmptyScanResult();
93+
}
5394

54-
logger.info("Found {} endpoints to scan", endpoints.size());
55-
56-
// Get applicable test cases
57-
List<TestCase> testCases = testCaseRegistry.getEnabledTestCases(config);
58-
logger.info("Running {} test cases", testCases.size());
59-
60-
// Run test cases against endpoints
61-
ExecutorService executor = Executors.newFixedThreadPool(config.getThreads());
62-
63-
for (EndpointInfo endpoint : endpoints) {
64-
for (TestCase testCase : testCases) {
65-
executor.submit(() -> {
66-
try {
67-
List<Finding> testFindings = testCase.execute(endpoint, httpClient);
68-
synchronized (findings) {
69-
findings.addAll(testFindings);
70-
}
71-
} catch (Exception e) {
72-
logger.error("Error executing test case {} on endpoint {}: {}",
73-
testCase.getId(), endpoint.getPath(), e.getMessage());
95+
// Get applicable test cases
96+
List<TestCase> testCases = testCaseRegistry.getEnabledTestCases(config);
97+
logger.info("Running {} test cases against {} endpoints", testCases.size(), endpoints.size());
98+
99+
// Calculate total tasks for progress tracking
100+
totalTasks.set(endpoints.size() * testCases.size());
101+
102+
// Run test cases against endpoints using virtual threads (Java 21)
103+
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
104+
List<CompletableFuture<Void>> futures = new ArrayList<>();
105+
106+
for (EndpointInfo endpoint : endpoints) {
107+
for (TestCase testCase : testCases) {
108+
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
109+
try {
110+
logger.debug("Executing {} on {}", testCase.getId(), endpoint);
111+
List<Finding> testFindings = testCase.execute(endpoint, httpClient);
112+
113+
if (!testFindings.isEmpty()) {
114+
synchronized (findings) {
115+
findings.addAll(testFindings);
116+
117+
// Update severity counters
118+
for (Finding finding : testFindings) {
119+
findingsBySeverity.get(finding.getSeverity()).incrementAndGet();
120+
}
121+
}
122+
123+
logger.debug("Found {} issues with {} on {}",
124+
testFindings.size(), testCase.getId(), endpoint);
125+
}
126+
} catch (Exception e) {
127+
logger.error("Error executing test case {} on endpoint {}: {}",
128+
testCase.getId(), endpoint.getPath(), e.getMessage());
129+
logger.debug("Exception details:", e);
130+
} finally {
131+
// Update progress
132+
int completed = completedTasks.incrementAndGet();
133+
if (completed % 10 == 0 || completed == totalTasks.get()) {
134+
logProgress();
135+
}
136+
}
137+
}, executor);
138+
139+
futures.add(future);
74140
}
75-
});
141+
}
142+
143+
// Wait for all tasks to complete or timeout
144+
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
145+
.orTimeout(config.getTimeoutMinutes(), TimeUnit.MINUTES)
146+
.exceptionally(ex -> {
147+
logger.warn("Scan interrupted or timed out before completion: {}", ex.getMessage());
148+
return null;
149+
})
150+
.join();
76151
}
77-
}
78152

79-
executor.shutdown();
80-
try {
81-
executor.awaitTermination(config.getTimeoutMinutes(), TimeUnit.MINUTES);
82-
} catch (InterruptedException e) {
83-
logger.warn("Scan interrupted before completion");
84-
Thread.currentThread().interrupt();
153+
logger.info("Scan completed. Found {} issues: {} critical, {} high, {} medium, {} low, {} info",
154+
findings.size(),
155+
findingsBySeverity.get(Severity.CRITICAL).get(),
156+
findingsBySeverity.get(Severity.HIGH).get(),
157+
findingsBySeverity.get(Severity.MEDIUM).get(),
158+
findingsBySeverity.get(Severity.LOW).get(),
159+
findingsBySeverity.get(Severity.INFO).get());
160+
161+
} catch (Exception e) {
162+
logger.error("Unhandled exception during scan: {}", e.getMessage());
163+
logger.debug("Exception details:", e);
85164
}
86165

166+
scanEndTime = LocalDateTime.now();
87167
ScanResult result = new ScanResult(config.getTargetUrl(), findings);
88-
logger.info("Scan completed. Found {} issues: {} high, {} medium, {} low severity",
89-
findings.size(),
90-
findings.stream().filter(f -> f.getSeverity() == Severity.HIGH).count(),
91-
findings.stream().filter(f -> f.getSeverity() == Severity.MEDIUM).count(),
92-
findings.stream().filter(f -> f.getSeverity() == Severity.LOW).count());
168+
result.setScanStartTime(scanStartTime);
169+
result.setScanEndTime(scanEndTime);
93170

94171
return result;
95172
}
96173

97174
/**
98175
* Attempts to discover API endpoints for the target.
99-
* This is a basic implementation that uses common paths and OpenAPI detection.
100176
*
101177
* @return A list of discovered endpoints
102178
*/
103179
private List<EndpointInfo> discoverEndpoints() {
104-
logger.info("Attempting to discover API endpoints");
105-
List<EndpointInfo> endpoints = new ArrayList<>();
106-
107-
// Try to find OpenAPI/Swagger specification
108-
List<String> specPaths = List.of(
109-
"/swagger/v1/swagger.json",
110-
"/swagger.json",
111-
"/api-docs",
112-
"/v2/api-docs",
113-
"/v3/api-docs",
114-
"/openapi.json"
115-
);
116-
117-
for (String path : specPaths) {
118-
try {
119-
String url = config.getTargetUrl() + path;
120-
String response = httpClient.get(url, Map.of());
121-
122-
if (response != null && !response.isEmpty()) {
123-
logger.info("Found potential API specification at: {}", url);
124-
// TODO: Parse OpenAPI spec and extract endpoints
125-
break;
126-
}
127-
} catch (Exception e) {
128-
// Continue with next path
129-
}
130-
}
180+
return discoveryService.discoverEndpoints();
181+
}
131182

132-
// If no endpoints were found through specifications, return some common ones for testing
133-
if (endpoints.isEmpty()) {
134-
logger.info("No API spec found, using common paths for testing");
135-
endpoints.add(new EndpointInfo("/api/v1/users", "GET"));
136-
endpoints.add(new EndpointInfo("/api/v1/users", "POST"));
137-
endpoints.add(new EndpointInfo("/api/v1/users/{id}", "GET"));
138-
endpoints.add(new EndpointInfo("/api/v1/auth/login", "POST"));
139-
endpoints.add(new EndpointInfo("/api/v1/products", "GET"));
140-
}
183+
/**
184+
* Logs the current progress of the scan.
185+
*/
186+
private void logProgress() {
187+
int completed = completedTasks.get();
188+
int total = totalTasks.get();
189+
double percentComplete = (double) completed / total * 100;
190+
191+
logger.info("Scan progress: {}% ({}/{} tasks completed)",
192+
String.format("%.1f", percentComplete), completed, total);
193+
}
141194

142-
return endpoints;
195+
/**
196+
* Creates an empty scan result when no endpoints are found.
197+
*
198+
* @return An empty scan result
199+
*/
200+
private ScanResult createEmptyScanResult() {
201+
scanEndTime = LocalDateTime.now();
202+
ScanResult result = new ScanResult(config.getTargetUrl(), List.of());
203+
result.setScanStartTime(scanStartTime);
204+
result.setScanEndTime(scanEndTime);
205+
return result;
143206
}
144207
}

0 commit comments

Comments
 (0)