diff --git a/LICENSE.md b/LICENSE.md
index 480cb22..5b66d9f 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1 +1,176 @@
-# TODO: Please update this file with the license of your project
\ No newline at end of file
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
\ No newline at end of file
diff --git a/README.md b/README.md
index e5bc360..2dcc231 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,14 @@ api-security-testing-framework/
For more detailed information, please refer to our [Documentation](docs/README.md).
+## Framework Overview
+
+For detailed understand on the framework, please refer to our [Framework Overview](docs/FRAMEWORK_OVERVIEW.md).
+
+## Architecture
+
+Please refer to our [Architecture](docs/ARCHITECTURE.md).
+
## Contributing
We welcome contributions from the community! Please see our [Contributing Guidelines](CONTRIBUTING.md) for more information on how to get involved.
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
new file mode 100644
index 0000000..5613f45
--- /dev/null
+++ b/docs/ARCHITECTURE.md
@@ -0,0 +1,183 @@
+# Framework Architecture
+
+## Overview
+
+The OWASP API Security Testing Framework is designed with a modular architecture to allow for extensibility and maintainability. This document outlines the high-level architecture and key components of the framework.
+
+## Component Architecture
+
+```
++----------------------------+
+| CLI Layer |
++----------------------------+
+ |
++----------------------------+
+| Scanner Core |
++----------------------------+
+ / | \
+ / | \
++--------+ +--------+ +--------+
+| HTTP | | Test | | Report |
+| Client | | Cases | | Engine |
++--------+ +--------+ +--------+
+```
+
+### Key Components
+
+1. **CLI Layer** (`org.owasp.astf.cli`)
+ - Parses command-line arguments
+ - Configures and initializes the scanner
+ - Handles user interaction
+ - Manages input/output
+
+2. **Scanner Core** (`org.owasp.astf.core`)
+ - Orchestrates the scanning process
+ - Manages test case execution
+ - Handles multi-threading and concurrency
+ - Collects and processes results
+
+3. **HTTP Client** (`org.owasp.astf.core.http`)
+ - Manages API communications
+ - Handles authentication
+ - Processes requests and responses
+ - Supports various HTTP methods and content types
+
+4. **Test Cases** (`org.owasp.astf.testcases`)
+ - Individual security test implementations
+ - Each test case targets specific vulnerability types
+ - Implements the TestCase interface
+ - Registered and managed by TestCaseRegistry
+
+5. **Report Engine** (`org.owasp.astf.reporting`)
+ - Generates reports in various formats (JSON, HTML, XML, SARIF)
+ - Formats findings with appropriate details
+ - Supports different output destinations
+
+6. **Integrations** (`org.owasp.astf.integrations`)
+ - CI/CD integration components
+ - External tool connectors
+ - Notification systems
+
+## Data Flow
+
+1. User invokes the CLI with scan parameters
+2. CLI configures the scanner with appropriate settings
+3. Scanner discovers or loads target endpoints
+4. For each endpoint, applicable test cases are executed
+5. Test cases use the HTTP client to make API requests
+6. Findings are collected by the scanner
+7. Report engine generates the requested output format
+8. Results are returned to the user
+
+## Key Interfaces
+
+### TestCase Interface
+
+```java
+public interface TestCase {
+ String getId();
+ String getName();
+ String getDescription();
+ List execute(EndpointInfo endpoint, HttpClient httpClient) throws IOException;
+}
+```
+
+All test cases implement this interface, allowing the scanner to execute them uniformly.
+
+### EndpointInfo Class
+
+```java
+public class EndpointInfo {
+ private String path;
+ private String method;
+ private String contentType;
+ private String requestBody;
+ private boolean requiresAuthentication;
+
+ // Constructors, getters, etc.
+}
+```
+
+Represents an API endpoint to be tested, including path, method, and metadata.
+
+### Finding Class
+
+```java
+public class Finding {
+ private String id;
+ private String title;
+ private String description;
+ private Severity severity;
+ private String testCaseId;
+ private String endpoint;
+ private String requestDetails;
+ private String responseDetails;
+ private String remediation;
+ private String evidence;
+
+ // Constructors, getters, etc.
+}
+```
+
+Represents a security finding with all relevant details.
+
+## Design Principles
+
+1. **Modularity**: Components are designed with clear boundaries
+2. **Extensibility**: Easy to add new test cases and functionality
+3. **Testability**: Components can be tested in isolation
+4. **Performance**: Efficient execution for large API surfaces
+5. **Usability**: Clear interfaces and documentation
+
+## Thread Model
+
+The scanner uses a thread pool to execute test cases concurrently:
+
+1. One thread per endpoint-testcase combination
+2. Configurable thread count via `--threads` option
+3. Uses Java 21 virtual threads for efficiency
+4. Results are synchronized to prevent race conditions
+
+## Adding New Test Cases
+
+To add a new test case:
+
+1. Create a class implementing the `TestCase` interface
+2. Implement the required methods
+3. Register the test case in `TestCaseRegistry.registerDefaultTestCases()`
+4. Add unit tests for the new test case
+
+Example:
+
+```java
+public class NewVulnerabilityTestCase implements TestCase {
+ @Override
+ public String getId() {
+ return "ASTF-API11-2023";
+ }
+
+ @Override
+ public String getName() {
+ return "New Vulnerability";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Tests for a new type of vulnerability";
+ }
+
+ @Override
+ public List execute(EndpointInfo endpoint, HttpClient httpClient) throws IOException {
+ // Implement vulnerability detection logic
+ // Return list of findings (or empty list if none found)
+ }
+}
+```
+
+## Future Architecture Enhancements
+
+1. Plugin system for custom test cases
+2. Distributed scanning capabilities
+3. Real-time reporting and notification
+4. Machine learning-based detection improvements
+5. Integration with vulnerability management platforms
\ No newline at end of file
diff --git a/docs/FRAMEWORK_OVERVIEW.md b/docs/FRAMEWORK_OVERVIEW.md
new file mode 100644
index 0000000..d7bb972
--- /dev/null
+++ b/docs/FRAMEWORK_OVERVIEW.md
@@ -0,0 +1,106 @@
+# OWASP API Security Testing Framework Overview
+
+## What This Framework Does
+
+The OWASP API Security Testing Framework (ASTF) is a comprehensive tool designed to identify security vulnerabilities in APIs based on the OWASP API Security Top 10. Unlike traditional security tools, ASTF specifically focuses on API-specific vulnerabilities that are often missed by general-purpose scanners.
+
+## How It Works
+
+The framework operates using a black-box testing approach:
+
+1. **Endpoint Discovery**: Automatically discovers API endpoints through:
+ - OpenAPI/Swagger specification parsing
+ - Common endpoint pattern testing
+ - Intelligent path traversal
+ - Manual endpoint specification
+
+2. **Security Testing**: Executes a comprehensive suite of tests targeting:
+ - API1:2023 - Broken Object Level Authorization
+ - API2:2023 - Broken Authentication
+ - API3:2023 - Excessive Data Exposure
+ - API4:2023 - Lack of Resources & Rate Limiting
+ - API5:2023 - Broken Function Level Authorization
+ - (Additional test cases in future releases)
+
+3. **Vulnerability Reporting**: Provides detailed findings including:
+ - Severity classification
+ - Vulnerability details
+ - Evidence capture
+ - Remediation guidance
+ - References to OWASP standards
+
+## Key Capabilities
+
+### Dynamic API Testing
+
+* Tests live API endpoints without needing source code
+* Detects vulnerabilities through intelligent request manipulation
+* Identifies security issues that affect APIs specifically
+
+### Authentication & Authorization Testing
+
+* Tests for improper access controls
+* Detects weak authentication mechanisms
+* Identifies JWT vulnerabilities
+* Tests for privilege escalation
+
+### Data Protection Analysis
+
+* Identifies sensitive data exposure
+* Detects missing encryption
+* Finds excessive data in responses
+
+### Resource Protection
+
+* Tests for missing rate limiting
+* Identifies DoS vulnerabilities
+* Detects resource consumption issues
+
+### Integration Capabilities
+
+* CI/CD pipeline integration
+* SARIF output for security dashboards
+* HTML reports for stakeholders
+* Command-line interface for scripting
+
+## Use Cases
+
+### Development Teams
+
+* Test APIs during development
+* Integrate security testing into CI/CD pipelines
+* Validate security controls before deployment
+
+### Security Teams
+
+* Assess API security posture
+* Validate vendor API security
+* Perform regular security assessments
+
+### DevSecOps
+
+* Automate API security testing
+* Generate compliance evidence
+* Track security improvements over time
+
+## Technical Architecture
+
+The framework is built on a modular Java architecture:
+
+* **Core Engine**: Manages test execution and coordination
+* **HTTP Client**: Handles API communications and request manipulation
+* **Test Cases**: Modular, extensible security tests
+* **Reporting Engine**: Generates findings in multiple formats
+* **CLI Interface**: Provides user interaction and configuration
+
+## Intended Audience
+
+The ASTF is designed for:
+
+* Security engineers
+* API developers
+* DevOps engineers
+* Security consultants
+* Quality assurance testers
+
+No deep security expertise is required to run basic scans, but security knowledge helps interpret results and implement fixes.
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index e901e5d..0ed2774 100644
--- a/pom.xml
+++ b/pom.xml
@@ -96,6 +96,18 @@
${junit.version}
test
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-yaml
+ ${jackson.version}
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+
diff --git a/src/main/java/org/owasp/astf/core/Scanner.java b/src/main/java/org/owasp/astf/core/Scanner.java
index 7ecad3d..3703b89 100644
--- a/src/main/java/org/owasp/astf/core/Scanner.java
+++ b/src/main/java/org/owasp/astf/core/Scanner.java
@@ -1,15 +1,20 @@
package org.owasp.astf.core;
+import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.astf.core.config.ScanConfig;
+import org.owasp.astf.core.discovery.EndpointDiscoveryService;
import org.owasp.astf.core.http.HttpClient;
import org.owasp.astf.core.result.Finding;
import org.owasp.astf.core.result.ScanResult;
@@ -19,6 +24,14 @@
/**
* The main scanner engine that orchestrates the API security testing process.
+ * This class is responsible for:
+ *
+ * - Initializing and executing the scan based on configuration
+ * - Managing endpoint discovery or using provided endpoints
+ * - Coordinating test case execution across endpoints
+ * - Collecting and aggregating findings
+ * - Providing progress updates and metrics
+ *
*/
public class Scanner {
private static final Logger logger = LogManager.getLogger(Scanner.class);
@@ -26,119 +39,169 @@ public class Scanner {
private final ScanConfig config;
private final HttpClient httpClient;
private final TestCaseRegistry testCaseRegistry;
+ private final EndpointDiscoveryService discoveryService;
+ // Scan metrics and tracking
+ private final AtomicInteger completedTasks = new AtomicInteger(0);
+ private final AtomicInteger totalTasks = new AtomicInteger(0);
+ private final Map findingsBySeverity = new ConcurrentHashMap<>();
+ private LocalDateTime scanStartTime;
+ private LocalDateTime scanEndTime;
+
+ /**
+ * Creates a new scanner with the specified configuration.
+ *
+ * @param config The scan configuration
+ */
public Scanner(ScanConfig config) {
this.config = config;
this.httpClient = new HttpClient(config);
this.testCaseRegistry = new TestCaseRegistry();
+ this.discoveryService = new EndpointDiscoveryService(config, httpClient);
+
+ // Initialize severity counters
+ for (Severity severity : Severity.values()) {
+ findingsBySeverity.put(severity, new AtomicInteger(0));
+ }
}
/**
* Executes a full scan based on the provided configuration.
*
- * @return The scan results containing all findings.
+ * @return The scan results containing all findings
*/
public ScanResult scan() {
- logger.info("Starting API security scan for target: {}", config.getTargetUrl());
-
+ scanStartTime = LocalDateTime.now();
List findings = new ArrayList<>();
- // Determine if we need to discover endpoints or use provided ones
- List endpoints = new ArrayList<>();
- if (config.isDiscoveryEnabled() && config.getEndpoints().isEmpty()) {
- endpoints = discoverEndpoints();
- } else {
- endpoints = config.getEndpoints();
- }
+ try {
+ logger.info("Starting API security scan for target: {}", config.getTargetUrl());
+
+ // Determine if we need to discover endpoints or use provided ones
+ List endpoints = new ArrayList<>();
+ if (config.isDiscoveryEnabled() && config.getEndpoints().isEmpty()) {
+ logger.info("No endpoints provided. Attempting endpoint discovery...");
+ endpoints = discoverEndpoints();
+ } else {
+ endpoints = config.getEndpoints();
+ logger.info("Using {} provided endpoints", endpoints.size());
+ }
+
+ if (endpoints.isEmpty()) {
+ logger.warn("No endpoints found to scan. Check target URL or provide endpoints manually.");
+ return createEmptyScanResult();
+ }
- logger.info("Found {} endpoints to scan", endpoints.size());
-
- // Get applicable test cases
- List testCases = testCaseRegistry.getEnabledTestCases(config);
- logger.info("Running {} test cases", testCases.size());
-
- // Run test cases against endpoints
- ExecutorService executor = Executors.newFixedThreadPool(config.getThreads());
-
- for (EndpointInfo endpoint : endpoints) {
- for (TestCase testCase : testCases) {
- executor.submit(() -> {
- try {
- List testFindings = testCase.execute(endpoint, httpClient);
- synchronized (findings) {
- findings.addAll(testFindings);
- }
- } catch (Exception e) {
- logger.error("Error executing test case {} on endpoint {}: {}",
- testCase.getId(), endpoint.getPath(), e.getMessage());
+ // Get applicable test cases
+ List testCases = testCaseRegistry.getEnabledTestCases(config);
+ logger.info("Running {} test cases against {} endpoints", testCases.size(), endpoints.size());
+
+ // Calculate total tasks for progress tracking
+ totalTasks.set(endpoints.size() * testCases.size());
+
+ // Run test cases against endpoints using virtual threads (Java 21)
+ try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
+ List> futures = new ArrayList<>();
+
+ for (EndpointInfo endpoint : endpoints) {
+ for (TestCase testCase : testCases) {
+ CompletableFuture future = CompletableFuture.runAsync(() -> {
+ try {
+ logger.debug("Executing {} on {}", testCase.getId(), endpoint);
+ List testFindings = testCase.execute(endpoint, httpClient);
+
+ if (!testFindings.isEmpty()) {
+ synchronized (findings) {
+ findings.addAll(testFindings);
+
+ // Update severity counters
+ for (Finding finding : testFindings) {
+ findingsBySeverity.get(finding.getSeverity()).incrementAndGet();
+ }
+ }
+
+ logger.debug("Found {} issues with {} on {}",
+ testFindings.size(), testCase.getId(), endpoint);
+ }
+ } catch (Exception e) {
+ logger.error("Error executing test case {} on endpoint {}: {}",
+ testCase.getId(), endpoint.getPath(), e.getMessage());
+ logger.debug("Exception details:", e);
+ } finally {
+ // Update progress
+ int completed = completedTasks.incrementAndGet();
+ if (completed % 10 == 0 || completed == totalTasks.get()) {
+ logProgress();
+ }
+ }
+ }, executor);
+
+ futures.add(future);
}
- });
+ }
+
+ // Wait for all tasks to complete or timeout
+ CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
+ .orTimeout(config.getTimeoutMinutes(), TimeUnit.MINUTES)
+ .exceptionally(ex -> {
+ logger.warn("Scan interrupted or timed out before completion: {}", ex.getMessage());
+ return null;
+ })
+ .join();
}
- }
- executor.shutdown();
- try {
- executor.awaitTermination(config.getTimeoutMinutes(), TimeUnit.MINUTES);
- } catch (InterruptedException e) {
- logger.warn("Scan interrupted before completion");
- Thread.currentThread().interrupt();
+ logger.info("Scan completed. Found {} issues: {} critical, {} high, {} medium, {} low, {} info",
+ findings.size(),
+ findingsBySeverity.get(Severity.CRITICAL).get(),
+ findingsBySeverity.get(Severity.HIGH).get(),
+ findingsBySeverity.get(Severity.MEDIUM).get(),
+ findingsBySeverity.get(Severity.LOW).get(),
+ findingsBySeverity.get(Severity.INFO).get());
+
+ } catch (Exception e) {
+ logger.error("Unhandled exception during scan: {}", e.getMessage());
+ logger.debug("Exception details:", e);
}
+ scanEndTime = LocalDateTime.now();
ScanResult result = new ScanResult(config.getTargetUrl(), findings);
- logger.info("Scan completed. Found {} issues: {} high, {} medium, {} low severity",
- findings.size(),
- findings.stream().filter(f -> f.getSeverity() == Severity.HIGH).count(),
- findings.stream().filter(f -> f.getSeverity() == Severity.MEDIUM).count(),
- findings.stream().filter(f -> f.getSeverity() == Severity.LOW).count());
+ result.setScanStartTime(scanStartTime);
+ result.setScanEndTime(scanEndTime);
return result;
}
/**
* Attempts to discover API endpoints for the target.
- * This is a basic implementation that uses common paths and OpenAPI detection.
*
* @return A list of discovered endpoints
*/
private List discoverEndpoints() {
- logger.info("Attempting to discover API endpoints");
- List endpoints = new ArrayList<>();
-
- // Try to find OpenAPI/Swagger specification
- List specPaths = List.of(
- "/swagger/v1/swagger.json",
- "/swagger.json",
- "/api-docs",
- "/v2/api-docs",
- "/v3/api-docs",
- "/openapi.json"
- );
-
- for (String path : specPaths) {
- try {
- String url = config.getTargetUrl() + path;
- String response = httpClient.get(url, Map.of());
-
- if (response != null && !response.isEmpty()) {
- logger.info("Found potential API specification at: {}", url);
- // TODO: Parse OpenAPI spec and extract endpoints
- break;
- }
- } catch (Exception e) {
- // Continue with next path
- }
- }
+ return discoveryService.discoverEndpoints();
+ }
- // If no endpoints were found through specifications, return some common ones for testing
- if (endpoints.isEmpty()) {
- logger.info("No API spec found, using common paths for testing");
- endpoints.add(new EndpointInfo("/api/v1/users", "GET"));
- endpoints.add(new EndpointInfo("/api/v1/users", "POST"));
- endpoints.add(new EndpointInfo("/api/v1/users/{id}", "GET"));
- endpoints.add(new EndpointInfo("/api/v1/auth/login", "POST"));
- endpoints.add(new EndpointInfo("/api/v1/products", "GET"));
- }
+ /**
+ * Logs the current progress of the scan.
+ */
+ private void logProgress() {
+ int completed = completedTasks.get();
+ int total = totalTasks.get();
+ double percentComplete = (double) completed / total * 100;
+
+ logger.info("Scan progress: {}% ({}/{} tasks completed)",
+ String.format("%.1f", percentComplete), completed, total);
+ }
- return endpoints;
+ /**
+ * Creates an empty scan result when no endpoints are found.
+ *
+ * @return An empty scan result
+ */
+ private ScanResult createEmptyScanResult() {
+ scanEndTime = LocalDateTime.now();
+ ScanResult result = new ScanResult(config.getTargetUrl(), List.of());
+ result.setScanStartTime(scanStartTime);
+ result.setScanEndTime(scanEndTime);
+ return result;
}
}
\ No newline at end of file
diff --git a/src/main/java/org/owasp/astf/core/config/ConfigLoader.java b/src/main/java/org/owasp/astf/core/config/ConfigLoader.java
new file mode 100644
index 0000000..10ac41f
--- /dev/null
+++ b/src/main/java/org/owasp/astf/core/config/ConfigLoader.java
@@ -0,0 +1,414 @@
+package org.owasp.astf.core.config;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.owasp.astf.core.EndpointInfo;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+
+/**
+ * Loads configuration from various sources such as files, environment variables,
+ * and command-line arguments.
+ *
+ * This class provides methods to:
+ *
+ * - Load configuration from YAML/JSON files
+ * - Load configuration from properties files
+ * - Load configuration from environment variables
+ * - Load endpoints from text files
+ * - Merge configurations from multiple sources
+ *
+ *
+ */
+public class ConfigLoader {
+ private static final Logger logger = LogManager.getLogger(ConfigLoader.class);
+
+ private final ObjectMapper jsonMapper;
+ private final ObjectMapper yamlMapper;
+
+ /**
+ * Creates a new configuration loader.
+ */
+ public ConfigLoader() {
+ this.jsonMapper = new ObjectMapper();
+ this.yamlMapper = new ObjectMapper(new YAMLFactory());
+ }
+
+ /**
+ * Loads a configuration from a YAML or JSON file.
+ *
+ * @param filePath The path to the configuration file
+ * @return The scan configuration
+ * @throws IOException If the file cannot be read or parsed
+ */
+ public ScanConfig loadFromFile(String filePath) throws IOException {
+ File file = new File(filePath);
+ if (!file.exists() || !file.isFile()) {
+ throw new IOException("Configuration file not found: " + filePath);
+ }
+
+ ScanConfig config = new ScanConfig();
+
+ // Determine file type by extension
+ if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
+ JsonNode root = yamlMapper.readTree(file);
+ parseJsonConfig(root, config);
+ } else if (filePath.endsWith(".json")) {
+ JsonNode root = jsonMapper.readTree(file);
+ parseJsonConfig(root, config);
+ } else if (filePath.endsWith(".properties")) {
+ Properties props = new Properties();
+ try (FileInputStream fis = new FileInputStream(file)) {
+ props.load(fis);
+ }
+ parsePropertiesConfig(props, config);
+ } else {
+ throw new IOException("Unsupported configuration file format: " + filePath);
+ }
+
+ logger.info("Loaded configuration from file: {}", filePath);
+ return config;
+ }
+
+ /**
+ * Loads endpoints from a text file.
+ *
+ * @param filePath The path to the endpoints file
+ * @return A list of endpoints
+ * @throws IOException If the file cannot be read
+ */
+ public List loadEndpointsFromFile(String filePath) throws IOException {
+ File file = new File(filePath);
+ if (!file.exists() || !file.isFile()) {
+ throw new IOException("Endpoints file not found: " + filePath);
+ }
+
+ List endpoints = new ArrayList<>();
+ List lines = Files.readAllLines(Paths.get(filePath));
+
+ for (String line : lines) {
+ // Skip comments and empty lines
+ line = line.trim();
+ if (line.isEmpty() || line.startsWith("#")) {
+ continue;
+ }
+
+ String[] parts = line.split("\\s+", 2);
+ if (parts.length == 2) {
+ String method = parts[0].trim();
+ String path = parts[1].trim();
+ endpoints.add(new EndpointInfo(path, method));
+ } else {
+ logger.warn("Invalid endpoint format: {}. Expected 'METHOD PATH'", line);
+ }
+ }
+
+ logger.info("Loaded {} endpoints from file: {}", endpoints.size(), filePath);
+ return endpoints;
+ }
+
+ /**
+ * Loads configuration from environment variables.
+ *
+ * @param prefix The prefix for environment variables (e.g., "ASTF_")
+ * @param config The configuration to update
+ */
+ public void loadFromEnvironment(String prefix, ScanConfig config) {
+ Map env = System.getenv();
+
+ // Process environment variables with the specified prefix
+ for (Map.Entry entry : env.entrySet()) {
+ String key = entry.getKey();
+ String value = entry.getValue();
+
+ if (key.startsWith(prefix)) {
+ String configKey = key.substring(prefix.length()).toLowerCase();
+ applyConfigValue(config, configKey, value);
+ }
+ }
+
+ logger.info("Loaded configuration from environment variables with prefix: {}", prefix);
+ }
+
+ /**
+ * Loads configuration from system properties.
+ *
+ * @param prefix The prefix for system properties (e.g., "astf.")
+ * @param config The configuration to update
+ */
+ public void loadFromSystemProperties(String prefix, ScanConfig config) {
+ Properties sysProps = System.getProperties();
+
+ // Process system properties with the specified prefix
+ for (String key : sysProps.stringPropertyNames()) {
+ if (key.startsWith(prefix)) {
+ String configKey = key.substring(prefix.length()).toLowerCase();
+ String value = sysProps.getProperty(key);
+ applyConfigValue(config, configKey, value);
+ }
+ }
+
+ logger.info("Loaded configuration from system properties with prefix: {}", prefix);
+ }
+
+ /**
+ * Parses a JSON/YAML configuration.
+ *
+ * @param root The root JSON node
+ * @param config The configuration to update
+ */
+ private void parseJsonConfig(JsonNode root, ScanConfig config) {
+ // Basic settings
+ if (root.has("targetUrl")) {
+ config.setTargetUrl(root.get("targetUrl").asText());
+ }
+
+ if (root.has("outputFile")) {
+ config.setOutputFile(root.get("outputFile").asText());
+ }
+
+ if (root.has("outputFormat")) {
+ String format = root.get("outputFormat").asText().toUpperCase();
+ try {
+ config.setOutputFormat(ScanConfig.OutputFormat.valueOf(format));
+ } catch (IllegalArgumentException e) {
+ logger.warn("Invalid output format: {}. Using default: {}", format, config.getOutputFormat());
+ }
+ }
+
+ if (root.has("threads")) {
+ config.setThreads(root.get("threads").asInt());
+ }
+
+ if (root.has("timeoutMinutes")) {
+ config.setTimeoutMinutes(root.get("timeoutMinutes").asInt());
+ }
+
+ if (root.has("discoveryEnabled")) {
+ config.setDiscoveryEnabled(root.get("discoveryEnabled").asBoolean());
+ }
+
+ if (root.has("verbose")) {
+ config.setVerbose(root.get("verbose").asBoolean());
+ }
+
+ // Headers
+ if (root.has("headers") && root.get("headers").isObject()) {
+ JsonNode headers = root.get("headers");
+ headers.fields().forEachRemaining(entry -> {
+ config.addHeader(entry.getKey(), entry.getValue().asText());
+ });
+ }
+
+ // Proxy settings
+ if (root.has("proxy")) {
+ JsonNode proxy = root.get("proxy");
+ if (proxy.has("host")) {
+ config.setProxyHost(proxy.get("host").asText());
+ }
+ if (proxy.has("port")) {
+ config.setProxyPort(proxy.get("port").asInt());
+ }
+ if (proxy.has("username")) {
+ config.setProxyUsername(proxy.get("username").asText());
+ }
+ if (proxy.has("password")) {
+ config.setProxyPassword(proxy.get("password").asText());
+ }
+ }
+
+ // Basic auth
+ if (root.has("basicAuth")) {
+ JsonNode basicAuth = root.get("basicAuth");
+ if (basicAuth.has("username")) {
+ config.setBasicAuthUsername(basicAuth.get("username").asText());
+ }
+ if (basicAuth.has("password")) {
+ config.setBasicAuthPassword(basicAuth.get("password").asText());
+ }
+ }
+
+ // Test case configuration
+ if (root.has("enableTestCases") && root.get("enableTestCases").isArray()) {
+ JsonNode enableTests = root.get("enableTestCases");
+ List enabledTestCaseIds = new ArrayList<>();
+ enableTests.forEach(node -> enabledTestCaseIds.add(node.asText()));
+ config.setEnabledTestCaseIds(enabledTestCaseIds);
+ }
+
+ if (root.has("disableTestCases") && root.get("disableTestCases").isArray()) {
+ JsonNode disableTests = root.get("disableTestCases");
+ List disabledTestCaseIds = new ArrayList<>();
+ disableTests.forEach(node -> disabledTestCaseIds.add(node.asText()));
+ config.setDisabledTestCaseIds(disabledTestCaseIds);
+ }
+ }
+
+ /**
+ * Parses a properties configuration.
+ *
+ * @param props The properties
+ * @param config The configuration to update
+ */
+ private void parsePropertiesConfig(Properties props, ScanConfig config) {
+ // Basic settings
+ if (props.containsKey("targetUrl")) {
+ config.setTargetUrl(props.getProperty("targetUrl"));
+ }
+
+ if (props.containsKey("outputFile")) {
+ config.setOutputFile(props.getProperty("outputFile"));
+ }
+
+ if (props.containsKey("outputFormat")) {
+ String format = props.getProperty("outputFormat").toUpperCase();
+ try {
+ config.setOutputFormat(ScanConfig.OutputFormat.valueOf(format));
+ } catch (IllegalArgumentException e) {
+ logger.warn("Invalid output format: {}. Using default: {}", format, config.getOutputFormat());
+ }
+ }
+
+ if (props.containsKey("threads")) {
+ config.setThreads(Integer.parseInt(props.getProperty("threads")));
+ }
+
+ if (props.containsKey("timeoutMinutes")) {
+ config.setTimeoutMinutes(Integer.parseInt(props.getProperty("timeoutMinutes")));
+ }
+
+ if (props.containsKey("discoveryEnabled")) {
+ config.setDiscoveryEnabled(Boolean.parseBoolean(props.getProperty("discoveryEnabled")));
+ }
+
+ if (props.containsKey("verbose")) {
+ config.setVerbose(Boolean.parseBoolean(props.getProperty("verbose")));
+ }
+
+ // Headers
+ for (String key : props.stringPropertyNames()) {
+ if (key.startsWith("header.")) {
+ String headerName = key.substring("header.".length());
+ config.addHeader(headerName, props.getProperty(key));
+ }
+ }
+
+ // Proxy settings
+ if (props.containsKey("proxy.host")) {
+ config.setProxyHost(props.getProperty("proxy.host"));
+ }
+ if (props.containsKey("proxy.port")) {
+ config.setProxyPort(Integer.parseInt(props.getProperty("proxy.port")));
+ }
+ if (props.containsKey("proxy.username")) {
+ config.setProxyUsername(props.getProperty("proxy.username"));
+ }
+ if (props.containsKey("proxy.password")) {
+ config.setProxyPassword(props.getProperty("proxy.password"));
+ }
+
+ // Basic auth
+ if (props.containsKey("basicAuth.username")) {
+ config.setBasicAuthUsername(props.getProperty("basicAuth.username"));
+ }
+ if (props.containsKey("basicAuth.password")) {
+ config.setBasicAuthPassword(props.getProperty("basicAuth.password"));
+ }
+
+ // Test case configuration
+ if (props.containsKey("enableTestCases")) {
+ String enableTests = props.getProperty("enableTestCases");
+ String[] testIds = enableTests.split(",");
+ List enabledTestCaseIds = new ArrayList<>();
+ for (String id : testIds) {
+ id = id.trim();
+ if (!id.isEmpty()) {
+ enabledTestCaseIds.add(id);
+ }
+ }
+ config.setEnabledTestCaseIds(enabledTestCaseIds);
+ }
+
+ if (props.containsKey("disableTestCases")) {
+ String disableTests = props.getProperty("disableTestCases");
+ String[] testIds = disableTests.split(",");
+ List disabledTestCaseIds = new ArrayList<>();
+ for (String id : testIds) {
+ id = id.trim();
+ if (!id.isEmpty()) {
+ disabledTestCaseIds.add(id);
+ }
+ }
+ config.setDisabledTestCaseIds(disabledTestCaseIds);
+ }
+ }
+
+ /**
+ * Applies a configuration value to the specified configuration.
+ *
+ * @param config The configuration to update
+ * @param key The configuration key
+ * @param value The configuration value
+ */
+ private void applyConfigValue(ScanConfig config, String key, String value) {
+ switch (key) {
+ case "targeturl" -> config.setTargetUrl(value);
+ case "outputfile" -> config.setOutputFile(value);
+ case "outputformat" -> {
+ try {
+ config.setOutputFormat(ScanConfig.OutputFormat.valueOf(value.toUpperCase()));
+ } catch (IllegalArgumentException e) {
+ logger.warn("Invalid output format: {}. Using default: {}", value, config.getOutputFormat());
+ }
+ }
+ case "threads" -> config.setThreads(Integer.parseInt(value));
+ case "timeoutminutes" -> config.setTimeoutMinutes(Integer.parseInt(value));
+ case "discoveryenabled" -> config.setDiscoveryEnabled(Boolean.parseBoolean(value));
+ case "verbose" -> config.setVerbose(Boolean.parseBoolean(value));
+ case "proxy_host" -> config.setProxyHost(value);
+ case "proxy_port" -> config.setProxyPort(Integer.parseInt(value));
+ case "proxy_username" -> config.setProxyUsername(value);
+ case "proxy_password" -> config.setProxyPassword(value);
+ case "basicauth_username" -> config.setBasicAuthUsername(value);
+ case "basicauth_password" -> config.setBasicAuthPassword(value);
+ default -> {
+ if (key.startsWith("header_")) {
+ String headerName = key.substring("header_".length());
+ config.addHeader(headerName, value);
+ } else if (key.equals("enabletestcases")) {
+ String[] testIds = value.split(",");
+ List enabledTestCaseIds = new ArrayList<>();
+ for (String id : testIds) {
+ id = id.trim();
+ if (!id.isEmpty()) {
+ enabledTestCaseIds.add(id);
+ }
+ }
+ config.setEnabledTestCaseIds(enabledTestCaseIds);
+ } else if (key.equals("disabletestcases")) {
+ String[] testIds = value.split(",");
+ List disabledTestCaseIds = new ArrayList<>();
+ for (String id : testIds) {
+ id = id.trim();
+ if (!id.isEmpty()) {
+ disabledTestCaseIds.add(id);
+ }
+ }
+ config.setDisabledTestCaseIds(disabledTestCaseIds);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/owasp/astf/core/config/ScanConfig.java b/src/main/java/org/owasp/astf/core/config/ScanConfig.java
index 6e40819..7e0820a 100644
--- a/src/main/java/org/owasp/astf/core/config/ScanConfig.java
+++ b/src/main/java/org/owasp/astf/core/config/ScanConfig.java
@@ -8,37 +8,81 @@
import org.owasp.astf.core.EndpointInfo;
/**
- * Configuration for a security scan.
+ * Configuration for an API security scan.
+ *
+ * This class contains all configuration parameters for executing a security scan,
+ * including target information, authentication settings, test case selection,
+ * threading options, output formats, and more.
+ *
*/
public class ScanConfig {
+ // Target and scope configuration
private String targetUrl;
- private Map headers;
private List endpoints;
- private int threads;
- private int timeoutMinutes;
- private boolean discoveryEnabled;
+ private boolean discoveryEnabled = true;
+ private List excludePatterns;
+ private Map headers;
+
+ // Authentication settings
+ private String basicAuthUsername;
+ private String basicAuthPassword;
+ private String apiKey;
+ private String apiKeyHeader = "X-API-Key";
+ private String bearerToken;
+
+ // Proxy settings
+ private String proxyHost;
+ private int proxyPort;
+ private String proxyUsername;
+ private String proxyPassword;
+
+ // Test case configuration
private List enabledTestCaseIds;
private List disabledTestCaseIds;
- private OutputFormat outputFormat;
+
+ // Execution settings
+ private int threads = 10;
+ private int timeoutMinutes = 30;
+ private int requestDelayMs = 0;
+ private int maxRequestsPerSecond = 0;
+ private boolean followRedirects = true;
+ private boolean validateCertificates = true;
+
+ // Output settings
+ private OutputFormat outputFormat = OutputFormat.JSON;
private String outputFile;
- private boolean verbose;
+ private boolean verbose = false;
+ private int maxFindings = 0;
+ private List excludeSeverities;
+ /**
+ * Creates a new scan configuration with default settings.
+ */
public ScanConfig() {
this.headers = new HashMap<>();
this.endpoints = new ArrayList<>();
- this.threads = 10;
- this.timeoutMinutes = 30;
- this.discoveryEnabled = true;
this.enabledTestCaseIds = new ArrayList<>();
this.disabledTestCaseIds = new ArrayList<>();
- this.outputFormat = OutputFormat.JSON;
- this.verbose = false;
+ this.excludePatterns = new ArrayList<>();
+ this.excludeSeverities = new ArrayList<>();
}
+ // Target and scope getters/setters
+
+ /**
+ * Gets the target URL for the API scan.
+ *
+ * @return The target URL
+ */
public String getTargetUrl() {
return targetUrl;
}
+ /**
+ * Sets the target URL for the API scan.
+ *
+ * @param targetUrl The target URL
+ */
public void setTargetUrl(String targetUrl) {
// Ensure target URL ends with a trailing slash
if (targetUrl != null && !targetUrl.endsWith("/")) {
@@ -48,98 +92,520 @@ public void setTargetUrl(String targetUrl) {
}
}
+ /**
+ * Gets the list of specific endpoints to scan.
+ *
+ * @return The endpoints to scan
+ */
+ public List getEndpoints() {
+ return endpoints;
+ }
+
+ /**
+ * Sets the list of specific endpoints to scan.
+ *
+ * @param endpoints The endpoints to scan
+ */
+ public void setEndpoints(List endpoints) {
+ this.endpoints = endpoints;
+ }
+
+ /**
+ * Adds an endpoint to scan.
+ *
+ * @param endpoint The endpoint to add
+ */
+ public void addEndpoint(EndpointInfo endpoint) {
+ this.endpoints.add(endpoint);
+ }
+
+ /**
+ * Checks if endpoint discovery is enabled.
+ *
+ * @return true if endpoint discovery is enabled
+ */
+ public boolean isDiscoveryEnabled() {
+ return discoveryEnabled;
+ }
+
+ /**
+ * Sets whether endpoint discovery is enabled.
+ *
+ * @param discoveryEnabled true to enable endpoint discovery
+ */
+ public void setDiscoveryEnabled(boolean discoveryEnabled) {
+ this.discoveryEnabled = discoveryEnabled;
+ }
+
+ /**
+ * Gets patterns to exclude from scanning.
+ *
+ * @return The exclude patterns
+ */
+ public List getExcludePatterns() {
+ return excludePatterns;
+ }
+
+ /**
+ * Sets patterns to exclude from scanning.
+ *
+ * @param excludePatterns The exclude patterns
+ */
+ public void setExcludePatterns(List excludePatterns) {
+ this.excludePatterns = excludePatterns;
+ }
+
+ /**
+ * Gets HTTP headers to include in requests.
+ *
+ * @return The HTTP headers
+ */
public Map getHeaders() {
return headers;
}
+ /**
+ * Sets HTTP headers to include in requests.
+ *
+ * @param headers The HTTP headers
+ */
public void setHeaders(Map headers) {
this.headers = headers;
}
+ /**
+ * Adds an HTTP header.
+ *
+ * @param name The header name
+ * @param value The header value
+ */
public void addHeader(String name, String value) {
this.headers.put(name, value);
}
- public List getEndpoints() {
- return endpoints;
+ // Authentication getters/setters
+
+ /**
+ * Gets the username for basic authentication.
+ *
+ * @return The basic auth username
+ */
+ public String getBasicAuthUsername() {
+ return basicAuthUsername;
}
- public void setEndpoints(List endpoints) {
- this.endpoints = endpoints;
+ /**
+ * Sets the username for basic authentication.
+ *
+ * @param basicAuthUsername The basic auth username
+ */
+ public void setBasicAuthUsername(String basicAuthUsername) {
+ this.basicAuthUsername = basicAuthUsername;
}
- public void addEndpoint(EndpointInfo endpoint) {
- this.endpoints.add(endpoint);
+ /**
+ * Gets the password for basic authentication.
+ *
+ * @return The basic auth password
+ */
+ public String getBasicAuthPassword() {
+ return basicAuthPassword;
}
- public int getThreads() {
- return threads;
+ /**
+ * Sets the password for basic authentication.
+ *
+ * @param basicAuthPassword The basic auth password
+ */
+ public void setBasicAuthPassword(String basicAuthPassword) {
+ this.basicAuthPassword = basicAuthPassword;
}
- public void setThreads(int threads) {
- this.threads = threads;
+ /**
+ * Gets the API key for authentication.
+ *
+ * @return The API key
+ */
+ public String getApiKey() {
+ return apiKey;
}
- public int getTimeoutMinutes() {
- return timeoutMinutes;
+ /**
+ * Sets the API key for authentication.
+ *
+ * @param apiKey The API key
+ */
+ public void setApiKey(String apiKey) {
+ this.apiKey = apiKey;
}
- public void setTimeoutMinutes(int timeoutMinutes) {
- this.timeoutMinutes = timeoutMinutes;
+ /**
+ * Gets the header name for the API key.
+ *
+ * @return The API key header name
+ */
+ public String getApiKeyHeader() {
+ return apiKeyHeader;
}
- public boolean isDiscoveryEnabled() {
- return discoveryEnabled;
+ /**
+ * Sets the header name for the API key.
+ *
+ * @param apiKeyHeader The API key header name
+ */
+ public void setApiKeyHeader(String apiKeyHeader) {
+ this.apiKeyHeader = apiKeyHeader;
}
- public void setDiscoveryEnabled(boolean discoveryEnabled) {
- this.discoveryEnabled = discoveryEnabled;
+ /**
+ * Gets the bearer token for authentication.
+ *
+ * @return The bearer token
+ */
+ public String getBearerToken() {
+ return bearerToken;
+ }
+
+ /**
+ * Sets the bearer token for authentication.
+ *
+ * @param bearerToken The bearer token
+ */
+ public void setBearerToken(String bearerToken) {
+ this.bearerToken = bearerToken;
+
+ // Automatically add the Authorization header if not present
+ if (bearerToken != null && !bearerToken.isEmpty() &&
+ !headers.containsKey("Authorization")) {
+ headers.put("Authorization", "Bearer " + bearerToken);
+ }
+ }
+
+ // Proxy getters/setters
+
+ /**
+ * Gets the proxy host.
+ *
+ * @return The proxy host
+ */
+ public String getProxyHost() {
+ return proxyHost;
}
+ /**
+ * Sets the proxy host.
+ *
+ * @param proxyHost The proxy host
+ */
+ public void setProxyHost(String proxyHost) {
+ this.proxyHost = proxyHost;
+ }
+
+ /**
+ * Gets the proxy port.
+ *
+ * @return The proxy port
+ */
+ public int getProxyPort() {
+ return proxyPort;
+ }
+
+ /**
+ * Sets the proxy port.
+ *
+ * @param proxyPort The proxy port
+ */
+ public void setProxyPort(int proxyPort) {
+ this.proxyPort = proxyPort;
+ }
+
+ /**
+ * Gets the proxy username for authentication.
+ *
+ * @return The proxy username
+ */
+ public String getProxyUsername() {
+ return proxyUsername;
+ }
+
+ /**
+ * Sets the proxy username for authentication.
+ *
+ * @param proxyUsername The proxy username
+ */
+ public void setProxyUsername(String proxyUsername) {
+ this.proxyUsername = proxyUsername;
+ }
+
+ /**
+ * Gets the proxy password for authentication.
+ *
+ * @return The proxy password
+ */
+ public String getProxyPassword() {
+ return proxyPassword;
+ }
+
+ /**
+ * Sets the proxy password for authentication.
+ *
+ * @param proxyPassword The proxy password
+ */
+ public void setProxyPassword(String proxyPassword) {
+ this.proxyPassword = proxyPassword;
+ }
+
+ // Test case configuration getters/setters
+
+ /**
+ * Gets the IDs of test cases to enable.
+ *
+ * @return The enabled test case IDs
+ */
public List getEnabledTestCaseIds() {
return enabledTestCaseIds;
}
+ /**
+ * Sets the IDs of test cases to enable.
+ *
+ * @param enabledTestCaseIds The enabled test case IDs
+ */
public void setEnabledTestCaseIds(List enabledTestCaseIds) {
this.enabledTestCaseIds = enabledTestCaseIds;
}
+ /**
+ * Gets the IDs of test cases to disable.
+ *
+ * @return The disabled test case IDs
+ */
public List getDisabledTestCaseIds() {
return disabledTestCaseIds;
}
+ /**
+ * Sets the IDs of test cases to disable.
+ *
+ * @param disabledTestCaseIds The disabled test case IDs
+ */
public void setDisabledTestCaseIds(List disabledTestCaseIds) {
this.disabledTestCaseIds = disabledTestCaseIds;
}
+ // Execution settings getters/setters
+
+ /**
+ * Gets the number of threads to use for scanning.
+ *
+ * @return The thread count
+ */
+ public int getThreads() {
+ return threads;
+ }
+
+ /**
+ * Sets the number of threads to use for scanning.
+ *
+ * @param threads The thread count
+ */
+ public void setThreads(int threads) {
+ this.threads = threads;
+ }
+
+ /**
+ * Gets the timeout for the scan in minutes.
+ *
+ * @return The timeout in minutes
+ */
+ public int getTimeoutMinutes() {
+ return timeoutMinutes;
+ }
+
+ /**
+ * Sets the timeout for the scan in minutes.
+ *
+ * @param timeoutMinutes The timeout in minutes
+ */
+ public void setTimeoutMinutes(int timeoutMinutes) {
+ this.timeoutMinutes = timeoutMinutes;
+ }
+
+ /**
+ * Gets the delay between requests in milliseconds.
+ *
+ * @return The request delay in milliseconds
+ */
+ public int getRequestDelayMs() {
+ return requestDelayMs;
+ }
+
+ /**
+ * Sets the delay between requests in milliseconds.
+ *
+ * @param requestDelayMs The request delay in milliseconds
+ */
+ public void setRequestDelayMs(int requestDelayMs) {
+ this.requestDelayMs = requestDelayMs;
+ }
+
+ /**
+ * Gets the maximum number of requests per second.
+ *
+ * @return The maximum requests per second
+ */
+ public int getMaxRequestsPerSecond() {
+ return maxRequestsPerSecond;
+ }
+
+ /**
+ * Sets the maximum number of requests per second.
+ *
+ * @param maxRequestsPerSecond The maximum requests per second
+ */
+ public void setMaxRequestsPerSecond(int maxRequestsPerSecond) {
+ this.maxRequestsPerSecond = maxRequestsPerSecond;
+ }
+
+ /**
+ * Checks if the client should follow redirects.
+ *
+ * @return true if redirects should be followed
+ */
+ public boolean isFollowRedirects() {
+ return followRedirects;
+ }
+
+ /**
+ * Sets whether the client should follow redirects.
+ *
+ * @param followRedirects true to follow redirects
+ */
+ public void setFollowRedirects(boolean followRedirects) {
+ this.followRedirects = followRedirects;
+ }
+
+ /**
+ * Checks if SSL certificates should be validated.
+ *
+ * @return true if certificates should be validated
+ */
+ public boolean isValidateCertificates() {
+ return validateCertificates;
+ }
+
+ /**
+ * Sets whether SSL certificates should be validated.
+ *
+ * @param validateCertificates true to validate certificates
+ */
+ public void setValidateCertificates(boolean validateCertificates) {
+ this.validateCertificates = validateCertificates;
+ }
+
+ // Output settings getters/setters
+
+ /**
+ * Gets the output format for the scan results.
+ *
+ * @return The output format
+ */
public OutputFormat getOutputFormat() {
return outputFormat;
}
+ /**
+ * Sets the output format for the scan results.
+ *
+ * @param outputFormat The output format
+ */
public void setOutputFormat(OutputFormat outputFormat) {
this.outputFormat = outputFormat;
}
+ /**
+ * Gets the output file path for the scan results.
+ *
+ * @return The output file path
+ */
public String getOutputFile() {
return outputFile;
}
+ /**
+ * Sets the output file path for the scan results.
+ *
+ * @param outputFile The output file path
+ */
public void setOutputFile(String outputFile) {
this.outputFile = outputFile;
}
+ /**
+ * Checks if verbose output is enabled.
+ *
+ * @return true if verbose output is enabled
+ */
public boolean isVerbose() {
return verbose;
}
+ /**
+ * Sets whether verbose output is enabled.
+ *
+ * @param verbose true to enable verbose output
+ */
public void setVerbose(boolean verbose) {
this.verbose = verbose;
}
+ /**
+ * Gets the maximum number of findings to include in the results.
+ *
+ * @return The maximum number of findings
+ */
+ public int getMaxFindings() {
+ return maxFindings;
+ }
+
+ /**
+ * Sets the maximum number of findings to include in the results.
+ *
+ * @param maxFindings The maximum number of findings
+ */
+ public void setMaxFindings(int maxFindings) {
+ this.maxFindings = maxFindings;
+ }
+
+ /**
+ * Gets the severities to exclude from the results.
+ *
+ * @return The excluded severities
+ */
+ public List getExcludeSeverities() {
+ return excludeSeverities;
+ }
+
+ /**
+ * Sets the severities to exclude from the results.
+ *
+ * @param excludeSeverities The excluded severities
+ */
+ public void setExcludeSeverities(List excludeSeverities) {
+ this.excludeSeverities = excludeSeverities;
+ }
+
+ /**
+ * Enumeration of supported output formats.
+ */
public enum OutputFormat {
+ /** JSON output format */
JSON,
+ /** XML output format */
XML,
+ /** HTML output format */
HTML,
+ /** SARIF output format for tool integration */
SARIF
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/org/owasp/astf/core/discovery/EndpointDiscoveryService.java b/src/main/java/org/owasp/astf/core/discovery/EndpointDiscoveryService.java
new file mode 100644
index 0000000..1becef1
--- /dev/null
+++ b/src/main/java/org/owasp/astf/core/discovery/EndpointDiscoveryService.java
@@ -0,0 +1,470 @@
+package org.owasp.astf.core.discovery;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.owasp.astf.core.EndpointInfo;
+import org.owasp.astf.core.config.ScanConfig;
+import org.owasp.astf.core.http.HttpClient;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Service responsible for discovering API endpoints through various methods.
+ *
+ * This class implements multiple discovery strategies:
+ *
+ * - OpenAPI/Swagger specification parsing
+ * - Common endpoint pattern testing
+ * - API root path exploration
+ * - Fallback to common endpoints for testing
+ *
+ *
+ */
+public class EndpointDiscoveryService {
+ private static final Logger logger = LogManager.getLogger(EndpointDiscoveryService.class);
+
+ private final ScanConfig config;
+ private final HttpClient httpClient;
+ private final ObjectMapper objectMapper;
+
+ // Paths where API specifications are commonly found
+ private static final List SPEC_PATHS = List.of(
+ "/swagger/v1/swagger.json",
+ "/swagger.json",
+ "/api-docs",
+ "/v2/api-docs",
+ "/v3/api-docs",
+ "/openapi.json",
+ "/swagger/index.html",
+ "/.well-known/openapi.json",
+ "/openapi/v3/api-docs",
+ "/api/swagger.json"
+ );
+
+ // Common API root paths to check
+ private static final List COMMON_API_ROOTS = List.of(
+ "/api",
+ "/api/v1",
+ "/api/v2",
+ "/rest",
+ "/service",
+ "/services",
+ "/v1",
+ "/v2"
+ );
+
+ // Common resource patterns for APIs
+ private static final List COMMON_RESOURCES = List.of(
+ "users",
+ "accounts",
+ "customers",
+ "products",
+ "orders",
+ "items",
+ "transactions",
+ "auth",
+ "login",
+ "register",
+ "profile",
+ "settings",
+ "admin",
+ "search",
+ "comments",
+ "posts",
+ "articles",
+ "categories",
+ "tags"
+ );
+
+ // Default HTTP methods to test
+ private static final List COMMON_METHODS = List.of(
+ "GET",
+ "POST",
+ "PUT",
+ "DELETE"
+ );
+
+ /**
+ * Creates a new endpoint discovery service.
+ *
+ * @param config The scan configuration
+ * @param httpClient The HTTP client for making requests
+ */
+ public EndpointDiscoveryService(ScanConfig config, HttpClient httpClient) {
+ this.config = config;
+ this.httpClient = httpClient;
+ this.objectMapper = new ObjectMapper();
+ }
+
+ /**
+ * Discovers API endpoints using multiple strategies.
+ *
+ * @return A list of discovered endpoints
+ */
+ public List discoverEndpoints() {
+ logger.info("Starting API endpoint discovery for: {}", config.getTargetUrl());
+ Set discoveredEndpoints = new HashSet<>();
+
+ try {
+ // Strategy 1: Try to find and parse OpenAPI/Swagger specifications
+ logger.debug("Attempting to discover OpenAPI/Swagger specifications");
+ List specEndpoints = discoverFromSpecifications();
+ if (!specEndpoints.isEmpty()) {
+ logger.info("Found {} endpoints from API specifications", specEndpoints.size());
+ discoveredEndpoints.addAll(specEndpoints);
+ return new ArrayList<>(discoveredEndpoints);
+ }
+
+ // Strategy 2: Explore common API roots
+ logger.debug("Exploring common API root paths");
+ List rootEndpoints = exploreApiRoots();
+ discoveredEndpoints.addAll(rootEndpoints);
+
+ // Strategy 3: Test common resource patterns
+ logger.debug("Testing common API resource patterns");
+ List resourceEndpoints = testCommonResourcePatterns();
+ discoveredEndpoints.addAll(resourceEndpoints);
+
+ if (discoveredEndpoints.isEmpty()) {
+ logger.warn("No endpoints discovered through automatic methods");
+ // Fallback strategy: Use common endpoints for testing
+ logger.info("Using fallback common endpoints for testing");
+ discoveredEndpoints.addAll(getFallbackEndpoints());
+ } else {
+ logger.info("Discovered {} unique endpoints", discoveredEndpoints.size());
+ }
+
+ } catch (Exception e) {
+ logger.error("Error during endpoint discovery: {}", e.getMessage());
+ logger.debug("Exception details:", e);
+ // Fallback to common endpoints on error
+ discoveredEndpoints.addAll(getFallbackEndpoints());
+ }
+
+ return new ArrayList<>(discoveredEndpoints);
+ }
+
+ /**
+ * Attempts to discover endpoints by finding and parsing OpenAPI/Swagger specifications.
+ *
+ * @return List of endpoints discovered from specifications
+ */
+ private List discoverFromSpecifications() {
+ List endpoints = new ArrayList<>();
+
+ for (String specPath : SPEC_PATHS) {
+ try {
+ String url = config.getTargetUrl() + specPath;
+ logger.debug("Checking for API specification at: {}", url);
+
+ String response = httpClient.get(url, Map.of());
+
+ if (response != null && !response.isEmpty()) {
+ logger.info("Found potential API specification at: {}", url);
+
+ // Check if it's a valid JSON response that might be an OpenAPI spec
+ if (isValidJson(response) && (response.contains("\"swagger\"") ||
+ response.contains("\"openapi\""))) {
+
+ // Parse the specification and extract endpoints
+ endpoints.addAll(parseOpenApiSpec(response));
+
+ if (!endpoints.isEmpty()) {
+ return endpoints;
+ }
+ } else if (response.contains(" parseOpenApiSpec(String specJson) {
+ List endpoints = new ArrayList<>();
+
+ try {
+ JsonNode root = objectMapper.readTree(specJson);
+
+ // Determine if this is Swagger 2.0 or OpenAPI 3.x
+ boolean isSwagger2 = root.has("swagger") && root.get("swagger").asText().startsWith("2");
+
+ if (isSwagger2) {
+ // Parse Swagger 2.0
+ JsonNode paths = root.path("paths");
+ if (paths.isObject()) {
+ paths.fields().forEachRemaining(entry -> {
+ String path = entry.getKey();
+ JsonNode pathItem = entry.getValue();
+
+ pathItem.fields().forEachRemaining(methodEntry -> {
+ String method = methodEntry.getKey().toUpperCase();
+ // Skip non-HTTP method fields
+ if (Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS")
+ .contains(method)) {
+
+ EndpointInfo endpoint = new EndpointInfo(path, method);
+ endpoints.add(endpoint);
+ }
+ });
+ });
+ }
+ } else {
+ // Parse OpenAPI 3.x
+ JsonNode paths = root.path("paths");
+ if (paths.isObject()) {
+ paths.fields().forEachRemaining(entry -> {
+ String path = entry.getKey();
+ JsonNode pathItem = entry.getValue();
+
+ pathItem.fields().forEachRemaining(methodEntry -> {
+ String method = methodEntry.getKey().toUpperCase();
+ // Skip non-HTTP method fields
+ if (Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS")
+ .contains(method)) {
+
+ boolean requiresAuth = false;
+
+ // Check for security requirements
+ JsonNode security = methodEntry.getValue().path("security");
+ if (!security.isMissingNode() && security.isArray() && security.size() > 0) {
+ requiresAuth = true;
+ }
+
+ EndpointInfo endpoint = new EndpointInfo(
+ path, method, "application/json", null, requiresAuth);
+ endpoints.add(endpoint);
+ }
+ });
+ });
+ }
+ }
+
+ logger.info("Extracted {} endpoints from OpenAPI specification", endpoints.size());
+
+ } catch (Exception e) {
+ logger.error("Error parsing OpenAPI specification: {}", e.getMessage());
+ }
+
+ return endpoints;
+ }
+
+ /**
+ * Extracts the specification URL from Swagger UI HTML.
+ *
+ * @param html The Swagger UI HTML
+ * @return The specification URL or null if not found
+ */
+ private String extractSpecUrlFromSwaggerUi(String html) {
+ // Look for the spec URL in the Swagger UI HTML
+ Pattern pattern = Pattern.compile("url:\\s*['\"]([^'\"]+)['\"]");
+ Matcher matcher = pattern.matcher(html);
+
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+
+ return null;
+ }
+
+ /**
+ * Extracts the base URL from a full URL.
+ *
+ * @param url The full URL
+ * @return The base URL
+ */
+ private String extractBaseUrl(String url) {
+ int pathStart = url.indexOf("/", 8); // Skip "http://" or "https://"
+ if (pathStart != -1) {
+ return url.substring(0, pathStart);
+ }
+ return url;
+ }
+
+ /**
+ * Explores common API root paths to find valid API endpoints.
+ *
+ * @return List of endpoints discovered from root paths
+ */
+ private List exploreApiRoots() {
+ List endpoints = new ArrayList<>();
+
+ for (String rootPath : COMMON_API_ROOTS) {
+ try {
+ String url = config.getTargetUrl() + rootPath;
+ logger.debug("Testing API root path: {}", url);
+
+ String response = httpClient.get(url, Map.of());
+
+ if (response != null && !response.isEmpty()) {
+ logger.info("Found potential API root at: {}", url);
+
+ // If we get a valid response, add the root path
+ endpoints.add(new EndpointInfo(rootPath, "GET"));
+
+ // If it's JSON, it might be an API that returns available endpoints
+ if (isValidJson(response)) {
+ try {
+ // Try to extract endpoints from the response
+ JsonNode root = objectMapper.readTree(response);
+ if (root.isArray() || (root.isObject() && root.size() > 0)) {
+ // This might be a listing of resources or endpoints
+ logger.debug("Root path returned JSON data, might contain API information");
+
+ // Add some child resource paths to test based on common patterns
+ for (String resource : COMMON_RESOURCES) {
+ endpoints.add(new EndpointInfo(rootPath + "/" + resource, "GET"));
+ }
+ }
+ } catch (Exception e) {
+ logger.debug("Error parsing root path response: {}", e.getMessage());
+ }
+ }
+ }
+ } catch (Exception e) {
+ logger.debug("Error testing root path {}: {}", rootPath, e.getMessage());
+ // Continue with next path
+ }
+ }
+
+ return endpoints;
+ }
+
+ /**
+ * Tests common API resource patterns to find valid endpoints.
+ *
+ * @return List of endpoints discovered from common patterns
+ */
+ private List testCommonResourcePatterns() {
+ List endpoints = new ArrayList<>();
+
+ // Combine API roots with common resources
+ for (String rootPath : COMMON_API_ROOTS) {
+ for (String resource : COMMON_RESOURCES) {
+ String resourcePath = rootPath + "/" + resource;
+
+ try {
+ String url = config.getTargetUrl() + resourcePath;
+ logger.debug("Testing resource path: {}", url);
+
+ String response = httpClient.get(url, Map.of());
+
+ if (response != null && !response.isEmpty() && !response.contains("error") &&
+ !response.contains("not found") && !response.contains("404")) {
+ logger.info("Found potential resource endpoint: {}", url);
+
+ // Add the base resource endpoint
+ endpoints.add(new EndpointInfo(resourcePath, "GET"));
+
+ // Add the resource with ID parameter for common methods
+ for (String method : COMMON_METHODS) {
+ endpoints.add(new EndpointInfo(resourcePath + "/{id}", method));
+ }
+
+ // If we found users, add some common user-related endpoints
+ if (resource.equals("users") || resource.equals("accounts")) {
+ endpoints.add(new EndpointInfo(resourcePath + "/me", "GET"));
+ endpoints.add(new EndpointInfo(resourcePath + "/{id}/profile", "GET"));
+ }
+
+ // If we found products, add some common product-related endpoints
+ if (resource.equals("products") || resource.equals("items")) {
+ endpoints.add(new EndpointInfo(resourcePath + "/search", "GET"));
+ endpoints.add(new EndpointInfo(resourcePath + "/categories", "GET"));
+ }
+ }
+ } catch (Exception e) {
+ logger.debug("Error testing resource path {}: {}", resourcePath, e.getMessage());
+ // Continue with next path
+ }
+ }
+ }
+
+ return endpoints;
+ }
+
+ /**
+ * Gets a list of common fallback endpoints to use when discovery fails.
+ *
+ * @return List of common API endpoints
+ */
+ private List getFallbackEndpoints() {
+ List endpoints = new ArrayList<>();
+
+ // Default base path
+ String basePath = "/api/v1";
+
+ // Add common endpoints for testing
+ endpoints.add(new EndpointInfo(basePath + "/users", "GET"));
+ endpoints.add(new EndpointInfo(basePath + "/users/{id}", "GET"));
+ endpoints.add(new EndpointInfo(basePath + "/users", "POST"));
+ endpoints.add(new EndpointInfo(basePath + "/users/{id}", "PUT"));
+ endpoints.add(new EndpointInfo(basePath + "/users/{id}", "DELETE"));
+
+ endpoints.add(new EndpointInfo(basePath + "/auth/login", "POST"));
+ endpoints.add(new EndpointInfo(basePath + "/auth/logout", "POST"));
+
+ endpoints.add(new EndpointInfo(basePath + "/products", "GET"));
+ endpoints.add(new EndpointInfo(basePath + "/products/{id}", "GET"));
+
+ endpoints.add(new EndpointInfo(basePath + "/orders", "GET"));
+ endpoints.add(new EndpointInfo(basePath + "/orders/{id}", "GET"));
+ endpoints.add(new EndpointInfo(basePath + "/orders", "POST"));
+
+ logger.info("Using {} fallback endpoints for testing", endpoints.size());
+ return endpoints;
+ }
+
+ /**
+ * Checks if a string is valid JSON.
+ *
+ * @param json The string to check
+ * @return true if the string is valid JSON
+ */
+ private boolean isValidJson(String json) {
+ try {
+ objectMapper.readTree(json);
+ return true;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/owasp/astf/core/http/HttpClient.java b/src/main/java/org/owasp/astf/core/http/HttpClient.java
index f52de26..85b5913 100644
--- a/src/main/java/org/owasp/astf/core/http/HttpClient.java
+++ b/src/main/java/org/owasp/astf/core/http/HttpClient.java
@@ -1,6 +1,12 @@
package org.owasp.astf.core.http;
import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -8,28 +14,70 @@
import org.apache.logging.log4j.Logger;
import org.owasp.astf.core.config.ScanConfig;
+import okhttp3.Authenticator;
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.ConnectionPool;
+import okhttp3.Cookie;
+import okhttp3.CookieJar;
+import okhttp3.Credentials;
+import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
+import okhttp3.ResponseBody;
/**
* HTTP client wrapper for making API requests.
+ *
+ * This class provides a robust HTTP client implementation that supports:
+ *
+ * - All common HTTP methods (GET, POST, PUT, DELETE, etc.)
+ * - Various authentication methods
+ * - Cookie handling
+ * - Proxy configuration
+ * - Connection pooling and timeout management
+ * - Response processing with headers
+ *
+ *
*/
public class HttpClient {
private static final Logger logger = LogManager.getLogger(HttpClient.class);
private final OkHttpClient client;
private final ScanConfig config;
+ private final Map defaultHeaders;
+ private final Map> cookieStore = new HashMap<>();
+ /**
+ * Creates a new HTTP client with the specified configuration.
+ *
+ * @param config The scan configuration
+ */
public HttpClient(ScanConfig config) {
this.config = config;
+ this.defaultHeaders = new HashMap<>(config.getHeaders());
OkHttpClient.Builder builder = new OkHttpClient.Builder()
- .connectTimeout(30, TimeUnit.SECONDS)
- .readTimeout(30, TimeUnit.SECONDS)
- .writeTimeout(30, TimeUnit.SECONDS);
+ .connectTimeout(Duration.ofSeconds(30))
+ .readTimeout(Duration.ofSeconds(30))
+ .writeTimeout(Duration.ofSeconds(30))
+ .connectionPool(new ConnectionPool(20, 5, TimeUnit.MINUTES))
+ .cookieJar(new InMemoryCookieJar())
+ .followRedirects(true)
+ .followSslRedirects(true);
+
+ // Configure proxy if specified
+ if (config.getProxyHost() != null && !config.getProxyHost().isEmpty()) {
+ configureProxy(builder);
+ }
+
+ // Configure basic authentication if specified
+ if (config.getBasicAuthUsername() != null && !config.getBasicAuthUsername().isEmpty()) {
+ configureBasicAuth(builder);
+ }
this.client = builder.build();
}
@@ -43,19 +91,7 @@ public HttpClient(ScanConfig config) {
* @throws IOException If the request fails
*/
public String get(String url, Map headers) throws IOException {
- Request.Builder requestBuilder = new Request.Builder()
- .url(url)
- .get();
-
- addHeaders(requestBuilder, headers);
-
- Request request = requestBuilder.build();
- try (Response response = client.newCall(request).execute()) {
- if (response.body() != null) {
- return response.body().string();
- }
- return "";
- }
+ return executeRequest(createRequest(url, "GET", headers, null, null));
}
/**
@@ -71,20 +107,7 @@ public String get(String url, Map headers) throws IOException {
public String post(String url, Map headers, String contentType, String body) throws IOException {
MediaType mediaType = MediaType.parse(contentType);
RequestBody requestBody = RequestBody.create(body, mediaType);
-
- Request.Builder requestBuilder = new Request.Builder()
- .url(url)
- .post(requestBody);
-
- addHeaders(requestBuilder, headers);
-
- Request request = requestBuilder.build();
- try (Response response = client.newCall(request).execute()) {
- if (response.body() != null) {
- return response.body().string();
- }
- return "";
- }
+ return executeRequest(createRequest(url, "POST", headers, mediaType, requestBody));
}
/**
@@ -100,20 +123,7 @@ public String post(String url, Map headers, String contentType,
public String put(String url, Map headers, String contentType, String body) throws IOException {
MediaType mediaType = MediaType.parse(contentType);
RequestBody requestBody = RequestBody.create(body, mediaType);
-
- Request.Builder requestBuilder = new Request.Builder()
- .url(url)
- .put(requestBody);
-
- addHeaders(requestBuilder, headers);
-
- Request request = requestBuilder.build();
- try (Response response = client.newCall(request).execute()) {
- if (response.body() != null) {
- return response.body().string();
- }
- return "";
- }
+ return executeRequest(createRequest(url, "PUT", headers, mediaType, requestBody));
}
/**
@@ -125,30 +135,120 @@ public String put(String url, Map headers, String contentType, S
* @throws IOException If the request fails
*/
public String delete(String url, Map headers) throws IOException {
- Request.Builder requestBuilder = new Request.Builder()
- .url(url)
- .delete();
+ return executeRequest(createRequest(url, "DELETE", headers, null, null));
+ }
- addHeaders(requestBuilder, headers);
+ /**
+ * Makes a PATCH request to the specified URL.
+ *
+ * @param url The target URL
+ * @param headers Additional headers to include
+ * @param contentType The content type of the request
+ * @param body The request body
+ * @return The response body as a string
+ * @throws IOException If the request fails
+ */
+ public String patch(String url, Map headers, String contentType, String body) throws IOException {
+ MediaType mediaType = MediaType.parse(contentType);
+ RequestBody requestBody = RequestBody.create(body, mediaType);
+ return executeRequest(createRequest(url, "PATCH", headers, mediaType, requestBody));
+ }
- Request request = requestBuilder.build();
- try (Response response = client.newCall(request).execute()) {
- if (response.body() != null) {
- return response.body().string();
+ /**
+ * Makes a HEAD request to the specified URL.
+ *
+ * @param url The target URL
+ * @param headers Additional headers to include
+ * @return The response headers
+ * @throws IOException If the request fails
+ */
+ public Map> head(String url, Map headers) throws IOException {
+ Response response = client.newCall(createRequest(url, "HEAD", headers, null, null)).execute();
+ try {
+ return extractHeaders(response);
+ } finally {
+ response.close();
+ }
+ }
+
+ /**
+ * Makes an asynchronous request to the specified URL.
+ *
+ * @param url The target URL
+ * @param method The HTTP method
+ * @param headers Additional headers to include
+ * @param contentType The content type of the request (null for GET, HEAD, DELETE)
+ * @param body The request body (null for GET, HEAD, DELETE)
+ * @param callback The callback to handle the response
+ */
+ public void asyncRequest(String url, String method, Map headers,
+ String contentType, String body, HttpResponseCallback callback) {
+ try {
+ MediaType mediaType = contentType != null ? MediaType.parse(contentType) : null;
+ RequestBody requestBody = null;
+
+ if (body != null && mediaType != null) {
+ requestBody = RequestBody.create(body, mediaType);
}
- return "";
+
+ Request request = createRequest(url, method, headers, mediaType, requestBody);
+
+ client.newCall(request).enqueue(new Callback() {
+ @Override
+ public void onFailure(Call call, IOException e) {
+ callback.onFailure(e);
+ }
+
+ @Override
+ public void onResponse(Call call, Response response) throws IOException {
+ try (ResponseBody responseBody = response.body()) {
+ String body = responseBody != null ? responseBody.string() : "";
+ Map> headers = extractHeaders(response);
+ int statusCode = response.code();
+
+ callback.onSuccess(statusCode, headers, body);
+ }
+ }
+ });
+ } catch (Exception e) {
+ callback.onFailure(e);
}
}
/**
- * Adds headers to the request builder.
+ * Creates an HTTP request with the specified parameters.
*
- * @param requestBuilder The request builder
+ * @param url The target URL
+ * @param method The HTTP method
* @param additionalHeaders Additional headers to include
+ * @param mediaType The media type of the request (null for GET, HEAD, DELETE)
+ * @param body The request body (null for GET, HEAD, DELETE)
+ * @return The HTTP request
*/
- private void addHeaders(Request.Builder requestBuilder, Map additionalHeaders) {
+ private Request createRequest(String url, String method, Map additionalHeaders,
+ MediaType mediaType, RequestBody body) {
+ Request.Builder requestBuilder = new Request.Builder()
+ .url(url);
+
+ // Set the appropriate method and body
+ switch (method.toUpperCase()) {
+ case "GET" -> requestBuilder.get();
+ case "HEAD" -> requestBuilder.head();
+ case "DELETE" -> requestBuilder.delete();
+ case "POST" -> requestBuilder.post(body);
+ case "PUT" -> requestBuilder.put(body);
+ case "PATCH" -> requestBuilder.patch(body);
+ default -> {
+ if (body != null) {
+ requestBuilder.method(method, body);
+ } else {
+ requestBuilder.method(method, null);
+ }
+ }
+ }
+
// Add default headers from config
- for (Map.Entry entry : config.getHeaders().entrySet()) {
+ for (Map.Entry entry : defaultHeaders.entrySet()) {
requestBuilder.header(entry.getKey(), entry.getValue());
}
@@ -158,5 +258,147 @@ private void addHeaders(Request.Builder requestBuilder, Map addi
requestBuilder.header(entry.getKey(), entry.getValue());
}
}
+
+ return requestBuilder.build();
+ }
+
+ /**
+ * Executes a request and returns the response body as a string.
+ *
+ * @param request The HTTP request to execute
+ * @return The response body as a string
+ * @throws IOException If the request fails
+ */
+ private String executeRequest(Request request) throws IOException {
+ try (Response response = client.newCall(request).execute()) {
+ if (response.body() != null) {
+ return response.body().string();
+ }
+ return "";
+ }
+ }
+
+ /**
+ * Extracts headers from a response.
+ *
+ * @param response The HTTP response
+ * @return A map of header names to values
+ */
+ private Map> extractHeaders(Response response) {
+ Map> headers = new HashMap<>();
+
+ for (String name : response.headers().names()) {
+ headers.put(name, response.headers(name));
+ }
+
+ return headers;
+ }
+
+ /**
+ * Configures proxy settings for the HTTP client.
+ *
+ * @param builder The OkHttpClient builder
+ */
+ private void configureProxy(OkHttpClient.Builder builder) {
+ Proxy proxy = new Proxy(
+ Proxy.Type.HTTP,
+ new InetSocketAddress(config.getProxyHost(), config.getProxyPort())
+ );
+
+ builder.proxy(proxy);
+
+ // Configure proxy authentication if needed
+ if (config.getProxyUsername() != null && !config.getProxyUsername().isEmpty()) {
+ Authenticator proxyAuthenticator = (route, response) -> {
+ String credential = Credentials.basic(config.getProxyUsername(), config.getProxyPassword());
+ return response.request().newBuilder()
+ .header("Proxy-Authorization", credential)
+ .build();
+ };
+
+ builder.proxyAuthenticator(proxyAuthenticator);
+ }
+ }
+
+ /**
+ * Configures basic authentication for the HTTP client.
+ *
+ * @param builder The OkHttpClient builder
+ */
+ private void configureBasicAuth(OkHttpClient.Builder builder) {
+ Authenticator authenticator = (route, response) -> {
+ String credential = Credentials.basic(config.getBasicAuthUsername(), config.getBasicAuthPassword());
+ return response.request().newBuilder()
+ .header("Authorization", credential)
+ .build();
+ };
+
+ builder.authenticator(authenticator);
+ }
+
+ /**
+ * In-memory cookie jar implementation for cookie management.
+ */
+ private class InMemoryCookieJar implements CookieJar {
+ @Override
+ public void saveFromResponse(HttpUrl url, List cookies) {
+ String domain = url.host();
+
+ if (!cookieStore.containsKey(domain)) {
+ cookieStore.put(domain, new ArrayList<>());
+ }
+
+ // Replace existing cookies with the same name
+ List domainCookies = cookieStore.get(domain);
+ for (Cookie cookie : cookies) {
+ // Remove existing cookie with same name if present
+ domainCookies.removeIf(existingCookie -> existingCookie.name().equals(cookie.name()));
+
+ // Add the new cookie
+ domainCookies.add(cookie);
+ }
+
+ logger.debug("Cookies for {}: {}", domain, domainCookies.size());
+ }
+
+ @Override
+ public List loadForRequest(HttpUrl url) {
+ String domain = url.host();
+ List validCookies = new ArrayList<>();
+
+ if (cookieStore.containsKey(domain)) {
+ List domainCookies = cookieStore.get(domain);
+ for (Cookie cookie : domainCookies) {
+ if (cookie.matches(url)) {
+ validCookies.add(cookie);
+ }
+ }
+ }
+
+ return validCookies;
+ }
+ }
+
+ /**
+ * Callback interface for asynchronous HTTP requests.
+ */
+ public interface HttpResponseCallback {
+ /**
+ * Called when the request is successful.
+ *
+ * @param statusCode The HTTP status code
+ * @param headers The response headers
+ * @param body The response body
+ */
+ void onSuccess(int statusCode, Map> headers, String body);
+
+ /**
+ * Called when the request fails.
+ *
+ * @param e The exception that caused the failure
+ */
+ void onFailure(IOException e);
+
+ void onFailure(Exception e);
}
}
\ No newline at end of file
diff --git a/src/main/java/org/owasp/astf/reporting/HtmlReportGenerator.java b/src/main/java/org/owasp/astf/reporting/HtmlReportGenerator.java
new file mode 100644
index 0000000..25a7aaf
--- /dev/null
+++ b/src/main/java/org/owasp/astf/reporting/HtmlReportGenerator.java
@@ -0,0 +1,264 @@
+package org.owasp.astf.reporting;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.owasp.astf.core.result.Finding;
+import org.owasp.astf.core.result.ScanResult;
+import org.owasp.astf.core.result.Severity;
+
+/**
+ * Generates HTML reports from scan results.
+ *
+ * This report generator creates human-readable HTML files with interactive
+ * features for exploring scan findings. The HTML report includes:
+ *
+ * - Executive summary with vulnerability statistics
+ * - Interactive finding details
+ * - Evidence and remediation guidance
+ * - Responsive design for various devices
+ *
+ *
+ */
+public class HtmlReportGenerator implements ReportGenerator {
+ private static final Logger logger = LogManager.getLogger(HtmlReportGenerator.class);
+
+ /**
+ * Creates a new HTML report generator with default settings.
+ */
+ public HtmlReportGenerator() {
+ // Default constructor
+ }
+
+ @Override
+ public void generateReport(ScanResult result, String outputPath) throws IOException {
+ logger.info("Generating HTML report at {}", outputPath);
+
+ StringBuilder html = new StringBuilder();
+
+ // Generate HTML header and styles
+ generateHtmlHeader(html);
+
+ // Generate summary section
+ generateSummarySection(html, result);
+
+ // Generate findings section
+ generateFindingsSection(html, result);
+
+ // Close HTML tags
+ html.append("\n