11package org .owasp .astf .core ;
22
3+ import java .time .LocalDateTime ;
34import java .util .ArrayList ;
45import java .util .List ;
56import java .util .Map ;
7+ import java .util .concurrent .CompletableFuture ;
8+ import java .util .concurrent .ConcurrentHashMap ;
69import java .util .concurrent .ExecutorService ;
710import java .util .concurrent .Executors ;
811import java .util .concurrent .TimeUnit ;
12+ import java .util .concurrent .atomic .AtomicInteger ;
913
1014import org .apache .logging .log4j .LogManager ;
1115import org .apache .logging .log4j .Logger ;
1216import org .owasp .astf .core .config .ScanConfig ;
17+ import org .owasp .astf .core .discovery .EndpointDiscoveryService ;
1318import org .owasp .astf .core .http .HttpClient ;
1419import org .owasp .astf .core .result .Finding ;
1520import org .owasp .astf .core .result .ScanResult ;
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 */
2336public 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