chore: auto-commit 2026-04-24 12:36
This commit is contained in:
commit
db41397368
192 changed files with 30366 additions and 0 deletions
36
AttackSurface/.gitignore
vendored
Normal file
36
AttackSurface/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Compiled binary (generated by build)
|
||||||
|
bin/aasd
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
*.log
|
||||||
|
server.out
|
||||||
|
integration.pid
|
||||||
|
|
||||||
|
# Generated reports (by GoTestWAF and discovery)
|
||||||
|
reports/*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
result/
|
||||||
|
.tmp/
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*.bak
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
1
AttackSurface/VERSION
Normal file
1
AttackSurface/VERSION
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
2026-04.1
|
||||||
BIN
AttackSurface/bin/gotestwaf
Executable file
BIN
AttackSurface/bin/gotestwaf
Executable file
Binary file not shown.
563
AttackSurface/bin/tools/README.md
Normal file
563
AttackSurface/bin/tools/README.md
Normal file
|
|
@ -0,0 +1,563 @@
|
||||||
|
# Subdomain Finder
|
||||||
|
|
||||||
|
[](https://github.com/valllabh/domain-scan/actions/workflows/ci.yml)
|
||||||
|
[](https://goreportcard.com/report/github.com/valllabh/domain-scan)
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
[](https://github.com/valllabh/domain-scan/releases)
|
||||||
|
|
||||||
|
A comprehensive Go-based tool for discovering and verifying active subdomains through multiple techniques including passive enumeration, TLS certificate analysis, and HTTP service verification.
|
||||||
|
|
||||||
|
## Built on Amazing Tools
|
||||||
|
|
||||||
|
This project stands on the shoulders of giants and integrates the excellent work from [ProjectDiscovery](https://projectdiscovery.io):
|
||||||
|
|
||||||
|
- **[subfinder](https://github.com/projectdiscovery/subfinder)** - Fast passive subdomain enumeration tool
|
||||||
|
- **[httpx](https://github.com/projectdiscovery/httpx)** - Fast and multi-purpose HTTP toolkit
|
||||||
|
|
||||||
|
### Why This Project Exists
|
||||||
|
|
||||||
|
While `subfinder` and `httpx` are incredibly powerful tools on their own, this project provides:
|
||||||
|
|
||||||
|
- **Unified Workflow**: Combines subdomain discovery, certificate analysis, and HTTP verification in one command
|
||||||
|
- **Keyword-Based Filtering**: Automatically extracts organization keywords from domains to filter SSL certificate SANs
|
||||||
|
- **Structured Output**: Provides consistent JSON/YAML output with detailed statistics and metadata
|
||||||
|
- **Library Integration**: Offers a Go library for programmatic use in other security tools
|
||||||
|
- **Queue-Based Processing**: Uses message queues for efficient concurrent domain processing
|
||||||
|
|
||||||
|
**TL;DR**: This is a wrapper that orchestrates `subfinder` and `httpx` with additional logic for enterprise security workflows.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download and install
|
||||||
|
curl -sSL https://github.com/valllabh/domain-scan/releases/latest/download/domain-scan_$(uname -s)_$(uname -m).tar.gz | tar -xz
|
||||||
|
sudo mv domain-scan /usr/local/bin/
|
||||||
|
|
||||||
|
# Run a basic scan
|
||||||
|
domain-scan discover example.com
|
||||||
|
|
||||||
|
# Or with Homebrew
|
||||||
|
brew install valllabh/tap/domain-scan
|
||||||
|
domain-scan discover example.com
|
||||||
|
|
||||||
|
# For development (from source)
|
||||||
|
git clone https://github.com/valllabh/domain-scan.git
|
||||||
|
cd domain-scan
|
||||||
|
make init && make run-discover
|
||||||
|
```
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This tool integrates [ProjectDiscovery's](https://projectdiscovery.io) security tools to perform comprehensive subdomain discovery and verification:
|
||||||
|
|
||||||
|
1. **Passive Discovery**: Uses [subfinder](https://github.com/projectdiscovery/subfinder) to enumerate subdomains from passive sources
|
||||||
|
2. **TLS Certificate Analysis**: Probes domains using [httpx](https://github.com/projectdiscovery/httpx) with TLS certificate inspection
|
||||||
|
3. **HTTP Service Verification**: Scans discovered subdomains for active HTTP/HTTPS services
|
||||||
|
4. **Keyword Extraction**: Automatically extracts keywords from domain names
|
||||||
|
5. **Keyword Filtering**: Filters domains from SSL certificates based on organizational relevance
|
||||||
|
6. **Deduplication**: Outputs unique active HTTP services
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The tool combines three powerful techniques for comprehensive subdomain discovery and verification:
|
||||||
|
|
||||||
|
### 1. Subfinder Integration
|
||||||
|
- Uses [ProjectDiscovery's subfinder](https://github.com/projectdiscovery/subfinder) to query passive DNS sources
|
||||||
|
- Discovers subdomains from certificate transparency logs, DNS records, and other sources
|
||||||
|
- Provides comprehensive initial subdomain enumeration without any active scanning
|
||||||
|
|
||||||
|
### 2. TLS Certificate Analysis
|
||||||
|
- Inspects Subject Alternative Names (SANs) in SSL/TLS certificates
|
||||||
|
- Finds additional subdomains not discovered by passive sources
|
||||||
|
- Filters domains based on organizational relevance using keywords
|
||||||
|
- Leverages certificate transparency for passive reconnaissance
|
||||||
|
|
||||||
|
#### SSL Certificate Keyword Filtering
|
||||||
|
When analyzing SSL certificates, domains often contain Subject Alternative Names (SANs) from multiple organizations due to shared hosting or third-party services. For example, if `apple.com` has a subdomain `status.apple.com` pointing to a third-party SaaS provider, that provider's certificate might also contain domains like `status.microsoft.com` or other unrelated organizations.
|
||||||
|
|
||||||
|
The keyword filtering system:
|
||||||
|
1. **Extracts keywords** from target domains (e.g., `apple.com` → `apple`, `iphone.com` → `iphone`)
|
||||||
|
2. **Filters certificate domains** to only include those matching organizational keywords
|
||||||
|
3. **Prevents noise** from unrelated domains in shared certificates
|
||||||
|
4. **Examples**:
|
||||||
|
- Target: `apple.com` → Keywords: `apple`
|
||||||
|
- Certificate contains: `status.apple.com`, `store.apple.com`, `status.microsoft.com`
|
||||||
|
- Filtered result: `status.apple.com`, `store.apple.com` (excludes `status.microsoft.com`)
|
||||||
|
|
||||||
|
### 3. HTTP Service Verification
|
||||||
|
- Scans all discovered subdomains for active HTTP/HTTPS services
|
||||||
|
- Tests multiple ports (configurable) for web services
|
||||||
|
- Verifies actual accessibility and responsiveness
|
||||||
|
- Returns only active, reachable services
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
- **Integrated Subfinder**: Built-in subfinder execution for comprehensive discovery
|
||||||
|
- **TLS Certificate Analysis**: Inspects Subject Alternative Names in SSL certificates with keyword filtering
|
||||||
|
- **HTTP Service Scanning**: Verifies active HTTP/HTTPS services on discovered subdomains
|
||||||
|
- **Configurable Port Scanning**: Customizable port list for HTTP service detection
|
||||||
|
- **Automatic Keyword Extraction**: Extracts keywords from domain names automatically
|
||||||
|
- **SSL Certificate Filtering**: Filters domains from SSL certificates based on organizational relevance
|
||||||
|
- **Concurrent Processing**: Uses httpx with configurable threads for fast scanning
|
||||||
|
- **Timeout Protection**: Configurable timeouts for reliable operation
|
||||||
|
- **Progress Indicators**: Real-time feedback on scanning progress
|
||||||
|
- **Deduplication**: Automatically removes duplicate subdomains
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
domain-scan discover [domains...] [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Basic Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get help
|
||||||
|
domain-scan --help
|
||||||
|
domain-scan discover --help
|
||||||
|
|
||||||
|
# Configuration management
|
||||||
|
domain-scan config show
|
||||||
|
domain-scan config init
|
||||||
|
domain-scan config set discovery.timeout 15
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Options
|
||||||
|
|
||||||
|
**Target and Keywords:**
|
||||||
|
- `domains`: Target domains for subdomain discovery
|
||||||
|
- `--keywords/-k`: Additional keywords for filtering SSL certificate domains (auto-extracted from domains and combined with provided keywords)
|
||||||
|
|
||||||
|
**Discovery Settings:**
|
||||||
|
- `--timeout`: Timeout in seconds (default: from config)
|
||||||
|
- `--threads`: Number of concurrent threads (default: from config)
|
||||||
|
|
||||||
|
**Output Options:**
|
||||||
|
- `--output/-o`: Output file path (default: stdout)
|
||||||
|
- `--format/-f`: Output format (text, json)
|
||||||
|
- `--result-dir`: Directory to save results (default: ./result)
|
||||||
|
- `--quiet/-q`: Suppress progress output
|
||||||
|
|
||||||
|
**Logging:**
|
||||||
|
- `--loglevel`: Log level (trace, debug, info, warn, error, silent)
|
||||||
|
- `--debug`: Enable debug logging (deprecated, use --loglevel debug)
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
- `--config`: Config file (default: $HOME/.domain-scan/config.yaml)
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic discovery (keywords automatically extracted from domain names)
|
||||||
|
domain-scan discover example.com
|
||||||
|
|
||||||
|
# Scan multiple domains
|
||||||
|
domain-scan discover example.com domain2.com
|
||||||
|
|
||||||
|
# Additional keywords (combined with auto-extracted ones)
|
||||||
|
domain-scan discover example.com --keywords staging,prod
|
||||||
|
|
||||||
|
# Custom discovery settings
|
||||||
|
domain-scan discover example.com --timeout 15 --threads 25
|
||||||
|
|
||||||
|
# Save results to JSON file
|
||||||
|
domain-scan discover example.com --output results.json --format json
|
||||||
|
|
||||||
|
# Custom result directory
|
||||||
|
domain-scan discover example.com --result-dir ./my-results
|
||||||
|
|
||||||
|
# Quiet mode with debug logging
|
||||||
|
domain-scan discover example.com --quiet --loglevel debug
|
||||||
|
|
||||||
|
# Multiple domains with custom settings
|
||||||
|
domain-scan discover example.com domain2.com --keywords api,admin --timeout 15
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Management
|
||||||
|
|
||||||
|
The tool supports configuration files for persistent settings:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Initialize default configuration
|
||||||
|
domain-scan config init
|
||||||
|
|
||||||
|
# View current configuration
|
||||||
|
domain-scan config show
|
||||||
|
|
||||||
|
# Set configuration values
|
||||||
|
domain-scan config set discovery.timeout 15
|
||||||
|
domain-scan config set keywords [mycompany,staging]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Default Ports:** 80, 443, 8080, 8443, 3000, 8000, 8888
|
||||||
|
**Configuration File:** `$HOME/.domain-scan/config.yaml`
|
||||||
|
|
||||||
|
## Integration with Main Project
|
||||||
|
|
||||||
|
This tool can be used standalone or integrated with the main reconnaissance script. The tool is self-contained and includes:
|
||||||
|
|
||||||
|
1. **Subfinder Integration**: Direct subfinder execution for comprehensive discovery
|
||||||
|
2. **Automatic Keyword Extraction**: No need for separate keyword extraction utilities
|
||||||
|
3. **TLS Certificate Analysis**: Additional discovery through certificate inspection with organizational filtering
|
||||||
|
4. **HTTP Service Verification**: Ensures only active services are reported
|
||||||
|
5. **Unified Output**: All active HTTP services in a single, deduplicated list
|
||||||
|
|
||||||
|
## Built-in Security Tools
|
||||||
|
|
||||||
|
This tool integrates the amazing security tools from [ProjectDiscovery](https://projectdiscovery.io):
|
||||||
|
|
||||||
|
- **[subfinder](https://github.com/projectdiscovery/subfinder)** - Fast passive subdomain enumeration tool
|
||||||
|
- **[httpx](https://github.com/projectdiscovery/httpx)** - Fast and multi-purpose HTTP toolkit for probing and TLS inspection
|
||||||
|
|
||||||
|
These tools are now integrated directly into domain-scan, so no separate installation is required.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The tool uses these default settings:
|
||||||
|
- **Timeout**: 10 seconds per domain
|
||||||
|
- **Threads**: 10 concurrent connections (TLS), 50 (HTTP scanning)
|
||||||
|
- **TLS Probe**: Enabled for certificate inspection
|
||||||
|
- **Subfinder**: Silent mode with all sources enabled
|
||||||
|
- **HTTP Scanning**: Tests both HTTP and HTTPS protocols
|
||||||
|
- **Default Ports**: 80,443,8080,8443,3000,8000,8888
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
This tool is designed for defensive security purposes:
|
||||||
|
- Performs passive reconnaissance and active HTTP probing
|
||||||
|
- No exploitation or intrusive testing beyond HTTP requests
|
||||||
|
- Helps organizations understand their external attack surface
|
||||||
|
- Complies with responsible disclosure practices
|
||||||
|
- Only tests for HTTP service availability
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
The tool outputs active HTTP services to stdout, one per line, making it easy to pipe to other tools or save to files. Progress information and statistics are sent to stderr.
|
||||||
|
|
||||||
|
**Example Output:**
|
||||||
|
```
|
||||||
|
https://store.example.com
|
||||||
|
http://staging.example.com:8080
|
||||||
|
https://staging.example.com:8443
|
||||||
|
```
|
||||||
|
|
||||||
|
## Progress Indicators
|
||||||
|
|
||||||
|
The tool provides real-time feedback through stderr:
|
||||||
|
- 🔍 Subdomain discovery progress
|
||||||
|
- 📋 Discovery statistics
|
||||||
|
- 🔐 TLS certificate analysis progress
|
||||||
|
- 🌐 HTTP service scanning progress
|
||||||
|
- ✅ Active service discoveries
|
||||||
|
- 📊 Final statistics
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Parse Arguments**: Extract target domains, keywords, and ports
|
||||||
|
2. **Keyword Extraction**: Auto-extract keywords from domain names and combine with any manually provided keywords
|
||||||
|
3. **Subfinder Discovery**: Run subfinder to get initial subdomain list
|
||||||
|
4. **TLS Certificate Analysis**: Probe domains for additional subdomains via certificate SANs, filtering by organizational relevance
|
||||||
|
5. **HTTP Service Scanning**: Test all discovered subdomains for active HTTP services
|
||||||
|
6. **SSL Certificate Filtering**: Filter certificate domains based on keyword relevance to target organization
|
||||||
|
7. **Deduplication**: Remove duplicate entries
|
||||||
|
8. **Output**: Print active HTTP services to stdout
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- TLS certificate analysis limited to domains with valid SSL/TLS certificates
|
||||||
|
- SSL certificate keyword filtering may exclude domains from shared certificates that don't match organizational keywords
|
||||||
|
- HTTP scanning limited to specified ports
|
||||||
|
- Performance depends on target domain response times and network connectivity
|
||||||
|
- Large subdomain lists may take considerable time to scan
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Binary Downloads
|
||||||
|
|
||||||
|
Download the latest release for your platform from the [releases page](https://github.com/valllabh/domain-scan/releases).
|
||||||
|
|
||||||
|
### Package Managers
|
||||||
|
|
||||||
|
#### Homebrew (macOS/Linux)
|
||||||
|
```bash
|
||||||
|
brew install valllabh/tap/domain-scan
|
||||||
|
```
|
||||||
|
|
||||||
|
#### APT (Debian/Ubuntu)
|
||||||
|
```bash
|
||||||
|
wget https://github.com/valllabh/domain-scan/releases/latest/download/domain-scan_amd64.deb
|
||||||
|
sudo dpkg -i domain-scan_amd64.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
#### RPM (RHEL/CentOS/Fedora)
|
||||||
|
```bash
|
||||||
|
wget https://github.com/valllabh/domain-scan/releases/latest/download/domain-scan_amd64.rpm
|
||||||
|
sudo rpm -i domain-scan_amd64.rpm
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Alpine Linux
|
||||||
|
```bash
|
||||||
|
wget https://github.com/valllabh/domain-scan/releases/latest/download/domain-scan_amd64.apk
|
||||||
|
sudo apk add --allow-untrusted domain-scan_amd64.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
### From Source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/valllabh/domain-scan@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
The tool includes comprehensive test coverage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
make test-coverage
|
||||||
|
|
||||||
|
# Run specific test packages
|
||||||
|
go test -v ./pkg/utils
|
||||||
|
go test -v ./pkg/discovery
|
||||||
|
go test -v ./pkg/domainscan
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
This section covers everything needed for developing and contributing to domain-scan.
|
||||||
|
|
||||||
|
**Quick Navigation:**
|
||||||
|
- [Building](#building)
|
||||||
|
- [Running During Development](#running-during-development)
|
||||||
|
- [Code Quality](#code-quality)
|
||||||
|
- [Make Commands Reference](#make-commands-reference)
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build for current platform
|
||||||
|
make build
|
||||||
|
|
||||||
|
# Build for all platforms
|
||||||
|
make build-all
|
||||||
|
|
||||||
|
# Development build with race detection
|
||||||
|
make dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running During Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run with custom arguments
|
||||||
|
make run ARGS="discover example.com --keywords staging,prod"
|
||||||
|
|
||||||
|
# Quick shortcuts for testing
|
||||||
|
make run-help # Show help
|
||||||
|
make run-discover # Test discovery with example.com
|
||||||
|
make run-config # Show current configuration
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
make run ARGS="discover example.com --keywords staging,prod --ports 80,443"
|
||||||
|
make run ARGS="discover test.com --format json --output results.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Format code
|
||||||
|
make fmt
|
||||||
|
|
||||||
|
# Lint code
|
||||||
|
make lint
|
||||||
|
|
||||||
|
# Security scan
|
||||||
|
make security
|
||||||
|
|
||||||
|
# Vulnerability check
|
||||||
|
make vuln
|
||||||
|
```
|
||||||
|
|
||||||
|
### Make Commands Reference
|
||||||
|
|
||||||
|
The project includes a comprehensive Makefile with targets for all development tasks. Run `make help` to see all available targets.
|
||||||
|
|
||||||
|
**Quick Start**: `make init && make test && make run-discover`
|
||||||
|
|
||||||
|
#### Build Targets
|
||||||
|
```bash
|
||||||
|
make build # Build for current platform
|
||||||
|
make build-all # Build for multiple platforms (Linux, macOS)
|
||||||
|
make dev # Development build with race detection
|
||||||
|
make clean # Clean build artifacts
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Development & Testing
|
||||||
|
```bash
|
||||||
|
make run ARGS="..." # Build and run with custom arguments
|
||||||
|
make run-help # Show application help
|
||||||
|
make run-discover # Quick test discovery with example.com
|
||||||
|
make run-config # Show current configuration
|
||||||
|
|
||||||
|
make test # Run all tests
|
||||||
|
make test-coverage # Run tests with HTML coverage report
|
||||||
|
make bench # Run benchmark tests
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Code Quality & Security
|
||||||
|
```bash
|
||||||
|
make fmt # Format code with gofmt
|
||||||
|
make lint # Run golangci-lint (installs if needed)
|
||||||
|
make security # Run gosec security scanner
|
||||||
|
make vuln # Check for vulnerabilities with govulncheck
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Dependencies & Environment
|
||||||
|
```bash
|
||||||
|
make deps # Install and verify Go dependencies
|
||||||
|
make init # Initialize development environment
|
||||||
|
make update # Update all dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Release & Distribution
|
||||||
|
```bash
|
||||||
|
make release # Create release using GoReleaser
|
||||||
|
make snapshot # Create snapshot release for testing
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Installation
|
||||||
|
```bash
|
||||||
|
make install # Install binary to $GOPATH/bin
|
||||||
|
make uninstall # Remove binary from $GOPATH/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Documentation & Help
|
||||||
|
```bash
|
||||||
|
make docs # Generate command documentation
|
||||||
|
make help # Show all available targets
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example Development Workflow
|
||||||
|
```bash
|
||||||
|
# Initialize environment
|
||||||
|
make init
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Test the application
|
||||||
|
make run-discover
|
||||||
|
|
||||||
|
# Test with custom arguments
|
||||||
|
make run ARGS="discover example.com --keywords api,admin --ports 80,443,8080"
|
||||||
|
|
||||||
|
# Format and lint code
|
||||||
|
make fmt lint
|
||||||
|
|
||||||
|
# Create a snapshot build
|
||||||
|
make snapshot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Release Process
|
||||||
|
|
||||||
|
This project uses automated releases via GitHub Actions and [GoReleaser](https://goreleaser.com/).
|
||||||
|
|
||||||
|
### Creating a Release
|
||||||
|
|
||||||
|
1. **Ensure all changes are on the main branch**
|
||||||
|
2. **Create and push a new tag**:
|
||||||
|
```bash
|
||||||
|
git tag v1.0.0
|
||||||
|
git push origin v1.0.0
|
||||||
|
```
|
||||||
|
3. **GitHub Actions will automatically**:
|
||||||
|
- Run all tests and quality checks
|
||||||
|
- Build binaries for multiple platforms (Linux, macOS)
|
||||||
|
- Create packages (DEB, RPM, APK)
|
||||||
|
- Generate release notes and publish to GitHub
|
||||||
|
- Update Homebrew formula
|
||||||
|
|
||||||
|
### Release Artifacts
|
||||||
|
|
||||||
|
Each release automatically produces:
|
||||||
|
|
||||||
|
- **Cross-platform binaries** for Linux and macOS (Intel/ARM)
|
||||||
|
- **Linux packages** (DEB, RPM, APK) for easy installation
|
||||||
|
- **Homebrew formula** for macOS and Linux users
|
||||||
|
- **Checksums** for artifact verification
|
||||||
|
|
||||||
|
### Versioning
|
||||||
|
|
||||||
|
The project follows [Semantic Versioning](https://semver.org/):
|
||||||
|
- `v1.0.0` - Major release with breaking changes
|
||||||
|
- `v1.1.0` - Minor release with new features
|
||||||
|
- `v1.1.1` - Patch release with bug fixes
|
||||||
|
|
||||||
|
### Development Releases
|
||||||
|
|
||||||
|
For testing unreleased changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a snapshot build locally
|
||||||
|
make snapshot
|
||||||
|
|
||||||
|
# Or use the manual test workflow in GitHub Actions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Common Issues:**
|
||||||
|
- Check network connectivity for target domains
|
||||||
|
- Verify domain names are correct and accessible
|
||||||
|
- For large subdomain lists, consider using smaller port ranges
|
||||||
|
- Monitor system resources during intensive scans
|
||||||
|
|
||||||
|
**Installation Issues:**
|
||||||
|
- For package installations, ensure you have appropriate permissions
|
||||||
|
- For Homebrew, run `brew update` if the formula isn't found
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
This tool is designed for defensive security and authorized reconnaissance only. Please review our [Security Policy](SECURITY.md) for:
|
||||||
|
|
||||||
|
- **Responsible usage guidelines**
|
||||||
|
- **Vulnerability reporting process**
|
||||||
|
- **Security best practices**
|
||||||
|
- **Dependency security information**
|
||||||
|
|
||||||
|
⚠️ **Important**: Only use this tool on systems you own or have explicit permission to test.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please:
|
||||||
|
|
||||||
|
1. Read our [Security Policy](SECURITY.md) first
|
||||||
|
2. Fork the repository
|
||||||
|
3. Create a feature branch
|
||||||
|
4. Make your changes with tests
|
||||||
|
5. Run `make test` and `make lint`
|
||||||
|
6. Submit a pull request
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
This project is built on the excellent work from the security community:
|
||||||
|
|
||||||
|
### Core Dependencies
|
||||||
|
- **[ProjectDiscovery](https://projectdiscovery.io)** - For creating amazing open source security tools
|
||||||
|
- **[subfinder](https://github.com/projectdiscovery/subfinder)** - Fast passive subdomain enumeration tool
|
||||||
|
- **[httpx](https://github.com/projectdiscovery/httpx)** - Fast and multi-purpose HTTP toolkit
|
||||||
|
- **[Cobra](https://github.com/spf13/cobra)** - CLI framework for Go
|
||||||
|
- **[Viper](https://github.com/spf13/viper)** - Configuration management for Go
|
||||||
|
|
||||||
|
### Special Thanks
|
||||||
|
- The [ProjectDiscovery](https://github.com/projectdiscovery) team for building the security tools that power this project
|
||||||
|
- The Go security community for creating robust security tooling
|
||||||
|
- All contributors who help improve this tool
|
||||||
|
|
||||||
|
💖 **If you find this tool useful, please star the repositories of the amazing tools it depends on!**
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
13
AttackSurface/bin/tools/config.yaml
Normal file
13
AttackSurface/bin/tools/config.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Domain-scan default configuration
|
||||||
|
discovery:
|
||||||
|
timeout: 10
|
||||||
|
threads: 50
|
||||||
|
|
||||||
|
ports:
|
||||||
|
default: [80, 443, 8080, 8443, 3000, 8000, 8888]
|
||||||
|
web: [80, 443, 8080, 8443]
|
||||||
|
dev: [3000, 8000, 8888, 9000]
|
||||||
|
enterprise: [80, 443, 8080, 8443, 8000, 9000, 8443]
|
||||||
|
|
||||||
|
keywords: []
|
||||||
|
|
||||||
BIN
AttackSurface/bin/tools/domain-scan
Executable file
BIN
AttackSurface/bin/tools/domain-scan
Executable file
Binary file not shown.
BIN
AttackSurface/bin/tools/httpx
Executable file
BIN
AttackSurface/bin/tools/httpx
Executable file
Binary file not shown.
BIN
AttackSurface/bin/tools/subfinder
Executable file
BIN
AttackSurface/bin/tools/subfinder
Executable file
Binary file not shown.
93
AttackSurface/docs/CHANGELOG.md
Normal file
93
AttackSurface/docs/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- GoTestWAF native report (`report_{token}.html`) now gets QR code injected for booth scanning
|
||||||
|
- SMTP configuration via environment variables (SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_FROM)
|
||||||
|
- Public base URL configuration via AASD_BASE_URL env var for QR codes and email links
|
||||||
|
- QR code injected into consultant-facing GoTestWAF report (non-clickable `<img>`, no link)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- QR code on user-facing report now points to consultant report (`report_{token}.html`) instead of itself
|
||||||
|
- Report link in simulation page no longer opens in new tab (removed `target="_blank"`)
|
||||||
|
- Booth CTA text: "Bring this code to BU 4" → "Show this code to Sechpoint Aftica Team"
|
||||||
|
- SMTP username corrected from `postpost@sechpoint.app` → `post@sechpoint.app`
|
||||||
|
- Email report link uses configurable base URL instead of hardcoded `http://localhost:8080`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Critical**: SMTP authentication failure — corrected typo in username (`postpost` → `post`)
|
||||||
|
- QR code encoding relative paths — now uses full public URL from AASD_BASE_URL
|
||||||
|
- Email report links hardcoded to localhost — now use configured base URL
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- "Email Report to Me" button and sendEmail JavaScript from user-facing report (prevented auto-email/spam concern)
|
||||||
|
- Hardcoded report link target in simulation page
|
||||||
|
|
||||||
|
## [0.3.0] - 2026-04-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Comprehensive email validation with regex pattern
|
||||||
|
- Domain security validation (path traversal prevention, length limits)
|
||||||
|
- Graceful shutdown with OS signal handling (SIGINT, SIGTERM)
|
||||||
|
- Context propagation for GoTestWAF scan cancellation
|
||||||
|
- Reports static file serving endpoint (`/reports/*`)
|
||||||
|
- Go 1.25.0 compiler support
|
||||||
|
- Scan status polling endpoint (`/scan-status/:domain`) for real-time updates
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- GoTestWAF command flags updated for compatibility:
|
||||||
|
- Removed invalid `--testCase "all"` flag
|
||||||
|
- Added `--reportFormat html` flag for HTML report generation
|
||||||
|
- Fixed report file naming consistency
|
||||||
|
- Improved error handling throughout application
|
||||||
|
- Enhanced code quality with Go idiomatic patterns
|
||||||
|
- Updated main.go with proper context management
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Critical**: GoTestWAF integration bugs causing scan failures
|
||||||
|
- **Critical**: Missing `--reportFormat` flag preventing HTML report generation
|
||||||
|
- **Critical**: Report file mismatch between stored results and generated files
|
||||||
|
- **Security**: Weak email validation allowing malformed input
|
||||||
|
- **Security**: Path traversal vulnerability in domain handling
|
||||||
|
- **Code Quality**: Monolithic function structure violating 50-line limit
|
||||||
|
- **Concurrency**: Potential race conditions in map access
|
||||||
|
- **Resource Leaks**: Goroutines not properly cancelled on shutdown
|
||||||
|
- **Missing Feature**: Reports not served via web endpoint
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Invalid GoTestWAF flag `--testCase "all"` (not supported in current version)
|
||||||
|
|
||||||
|
## [0.2.0] - 2026-04-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Gin web framework dependency installed
|
||||||
|
- POST `/start` endpoint with email domain extraction and validation
|
||||||
|
- Static file serving (`/static`, `/`, `/simulation`)
|
||||||
|
- In‑memory storage for scan results with thread‑safe mutex
|
||||||
|
- Basic HTML frontend: capture page (`index.html`) with QR placeholder
|
||||||
|
- Simulation page (`simulation.html`) with JavaScript step sequencer
|
||||||
|
- Consultant dashboard (`/admin‑dashboard`) with results table
|
||||||
|
- GoTestWAF binary integration (background execution with flags)
|
||||||
|
- Reports directory auto‑creation
|
||||||
|
- Server listens on `0.0.0.0:8080` for booth Wi‑Fi access
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Project structure refined: `AttackSurface/{bin,src,docs}`
|
||||||
|
- Git repository initialized with `main` branch
|
||||||
|
- Go 1.24.4 installed via official binary
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
## [0.1.0] - 2026-04-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Project initialization based on PROJECT_PLAN.md
|
||||||
|
- Basic directory structure for Go web application
|
||||||
|
- GoTestWAF binary integration (planned)
|
||||||
103
AttackSurface/docs/DEVELOPMENT_STATUS.md
Normal file
103
AttackSurface/docs/DEVELOPMENT_STATUS.md
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
# Development Status Summary
|
||||||
|
|
||||||
|
**Current Version**: 0.3.0 (Released: 2026-04-22)
|
||||||
|
|
||||||
|
The Resilience Challenge application is now **production-ready** for Phase 5 testing with your Wallarm-protected server (`https://git.sechpoint.app`).
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
✅ **Phase 1-4 Complete**: All foundational development phases completed
|
||||||
|
✅ **Critical Bug Fixes**: 9 major issues resolved in comprehensive code review
|
||||||
|
✅ **Security Hardening**: Multiple security vulnerabilities patched
|
||||||
|
✅ **Production Readiness**: Application compiled, tested, and ready for booth deployment
|
||||||
|
|
||||||
|
## Key Features Operational
|
||||||
|
|
||||||
|
| Feature | Status | Details |
|
||||||
|
|---------|--------|---------|
|
||||||
|
| **Email Capture & Processing** | ✅ Ready | Regex validation, domain extraction, security checks |
|
||||||
|
| **GoTestWAF Integration** | ✅ Ready | Background scanning with 120-second timeout |
|
||||||
|
| **Real-time Status Tracking** | ✅ Ready | Polling endpoint `/scan-status/:domain` |
|
||||||
|
| **Admin Dashboard** | ✅ Ready | Consultant view of all scan results |
|
||||||
|
| **Report Generation & Serving** | ✅ Ready | HTML reports at `/reports/report_*.html` |
|
||||||
|
| **Graceful Shutdown** | ✅ Ready | OS signal handling (SIGINT, SIGTERM) |
|
||||||
|
| **Booth Network Support** | ✅ Ready | Binds to `0.0.0.0:8080` for Wi-Fi access |
|
||||||
|
|
||||||
|
## Critical Issues Fixed
|
||||||
|
|
||||||
|
### 🔴 **GoTestWAF Integration** (Previously Broken)
|
||||||
|
- **Invalid flag `--testCase "all"`** removed (not supported in current version)
|
||||||
|
- **Missing `--reportFormat html`** added for proper HTML report generation
|
||||||
|
- **Report file mismatch** resolved between stored results and generated files
|
||||||
|
- **Binary path resolution** fixed for reliable execution
|
||||||
|
|
||||||
|
### 🔴 **Security Vulnerabilities** (Now Patched)
|
||||||
|
- **Weak email validation** → Comprehensive regex validation added
|
||||||
|
- **Path traversal risk** → Domain character validation prevents `/` and `\`
|
||||||
|
- **Input sanitization** → Length limits (255 chars) and format enforcement
|
||||||
|
- **Error information leakage** → Proper error handling without internal details
|
||||||
|
|
||||||
|
### 🔴 **Code Quality Issues** (Now Resolved)
|
||||||
|
- **Monolithic structure** → Refactored with proper context propagation
|
||||||
|
- **Resource leaks** → Goroutines properly cancelled on shutdown
|
||||||
|
- **Concurrency safety** → Improved mutex usage and race condition prevention
|
||||||
|
- **Error handling** → Graceful shutdown with 10-second timeout
|
||||||
|
|
||||||
|
## Technical Specifications
|
||||||
|
|
||||||
|
- **Language**: Go 1.25.0
|
||||||
|
- **Framework**: Gin web framework
|
||||||
|
- **Target Server**: `https://git.sechpoint.app` (your Wallarm-protected server)
|
||||||
|
- **Network Binding**: `0.0.0.0:8080` (all interfaces for booth Wi-Fi)
|
||||||
|
- **Scan Timeout**: 120 seconds per domain
|
||||||
|
- **Report Format**: HTML (generated by GoTestWAF)
|
||||||
|
- **Data Storage**: In-memory map (volatile, resets on restart)
|
||||||
|
|
||||||
|
## Testing Results
|
||||||
|
|
||||||
|
| Test | Result | Notes |
|
||||||
|
|------|--------|-------|
|
||||||
|
| **Compilation** | ✅ Success | Go 1.25.0 compatible |
|
||||||
|
| **Server Startup** | ✅ Success | Binds to `0.0.0.0:8080` |
|
||||||
|
| **Frontend Loading** | ✅ Success | All pages load correctly |
|
||||||
|
| **API Endpoints** | ✅ Success | POST `/start`, GET `/scan-status`, etc. |
|
||||||
|
| **Report Serving** | ✅ Success | Static files served at `/reports/*` |
|
||||||
|
| **Graceful Shutdown** | ✅ Success | SIGINT/SIGTERM handled properly |
|
||||||
|
|
||||||
|
## Ready for Phase 5 Testing
|
||||||
|
|
||||||
|
### Deployment Instructions:
|
||||||
|
```bash
|
||||||
|
cd gitex2026/AttackSurface
|
||||||
|
./start.sh # Starts server with logging
|
||||||
|
```
|
||||||
|
|
||||||
|
### Access Points:
|
||||||
|
- **Frontend**: `http://localhost:8080` (or booth Wi-Fi IP)
|
||||||
|
- **Admin Dashboard**: `http://localhost:8080/admin-dashboard`
|
||||||
|
- **Reports**: `http://localhost:8080/reports/report_*.html`
|
||||||
|
|
||||||
|
### Test Flow:
|
||||||
|
1. Submit email at booth → Domain extracted → GoTestWAF scan initiated
|
||||||
|
2. Real-time status updates via frontend polling
|
||||||
|
3. HTML report generated upon completion
|
||||||
|
4. Consultant monitors all scans via admin dashboard
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
The application is now **fully functional** and ready for:
|
||||||
|
1. **Integration testing** with your Wallarm filtering node
|
||||||
|
2. **Performance validation** (30-60 second scan targets)
|
||||||
|
3. **Booth deployment** for GITEX 2026 event
|
||||||
|
4. **User acceptance testing** with actual booth visitors
|
||||||
|
|
||||||
|
**All critical bugs have been resolved. The application meets production standards for security, reliability, and maintainability.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [CHANGELOG.md](CHANGELOG.md) - Version history and detailed changes
|
||||||
|
- [README.md](../../README.md) - Project overview and quick start guide
|
||||||
|
|
||||||
|
*Last Updated: 2026-04-22*
|
||||||
45
AttackSurface/src/.gitignore
vendored
Normal file
45
AttackSurface/src/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Reports directory (generated by GoTestWAF)
|
||||||
|
reports/*
|
||||||
|
|
||||||
|
# Local development configuration
|
||||||
|
config.yaml
|
||||||
|
config.local.yaml
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
281
AttackSurface/src/cmd/aasd/main.go
Normal file
281
AttackSurface/src/cmd/aasd/main.go
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
// AASD — API Attack Surface Discovery
|
||||||
|
//
|
||||||
|
// Version: 2026-04.1
|
||||||
|
// Booth-ready application for GITEX 2026.
|
||||||
|
//
|
||||||
|
// Workflow:
|
||||||
|
// Email Input → Domain Discovery (domain-scan) → Filter Subdomains →
|
||||||
|
// GoTestWAF Scan → DeepSeek AI Narrative → Static HTML Report (UUID) →
|
||||||
|
// Email Report (SMTP button on report page)
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/skip2/go-qrcode"
|
||||||
|
|
||||||
|
"aasd/internal/ai"
|
||||||
|
"aasd/internal/mailer"
|
||||||
|
"aasd/internal/scanner"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
emailRegex = regexp.MustCompile(`^[^@]+@[^@]+\.[^@]+$`)
|
||||||
|
projectDir string
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Determine project root (where bin/ and reports/ live)
|
||||||
|
var err error
|
||||||
|
projectDir, err = os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
projectDir = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read public base URL from environment (used for QR codes, email links)
|
||||||
|
// e.g. AASD_BASE_URL=https://coding.sechpoint.app
|
||||||
|
baseURL := os.Getenv("AASD_BASE_URL")
|
||||||
|
|
||||||
|
// Initialize services
|
||||||
|
deepSeekClient := ai.NewClient("sk-2b898e303be94266a060c8f3a0ca205c")
|
||||||
|
mailerService := mailer.New(mailer.ConfigFromEnv())
|
||||||
|
orch := scanner.NewOrchestrator(projectDir, baseURL, deepSeekClient, mailerService)
|
||||||
|
|
||||||
|
// Create main context for graceful shutdown
|
||||||
|
mainCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
router := gin.Default()
|
||||||
|
router.LoadHTMLGlob("src/templates/*")
|
||||||
|
|
||||||
|
// Serve static files
|
||||||
|
router.Static("/static", "./src/static")
|
||||||
|
router.StaticFile("/", "./src/static/index.html")
|
||||||
|
router.Static("/reports", "./reports")
|
||||||
|
|
||||||
|
// QR code generator endpoint
|
||||||
|
router.GET("/qrcode", func(c *gin.Context) {
|
||||||
|
text := c.Query("text")
|
||||||
|
if text == "" {
|
||||||
|
text = "AASD-2026"
|
||||||
|
}
|
||||||
|
png, err := qrcode.Encode(text, qrcode.Medium, 256)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate QR code"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Data(http.StatusOK, "image/png", png)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----- START: Pipeline Initiation -----
|
||||||
|
router.POST("/start", func(c *gin.Context) {
|
||||||
|
email := strings.TrimSpace(c.PostForm("email"))
|
||||||
|
if email == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Email is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !emailRegex.MatchString(email) {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid email format"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(email, "@")
|
||||||
|
domain := parts[1]
|
||||||
|
if !strings.Contains(domain, ".") {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid domain"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.ContainsAny(domain, "/\\") {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid domain characters"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(domain) > 255 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Domain too long"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start pipeline
|
||||||
|
result, err := orch.StartPipeline(mainCtx, email, domain)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to start scan"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to simulation page with token
|
||||||
|
c.Redirect(http.StatusFound, "/simulation?token="+result.ReportToken)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----- STATUS: Poll endpoint -----
|
||||||
|
router.GET("/scan-status/:token", func(c *gin.Context) {
|
||||||
|
token := c.Param("token")
|
||||||
|
|
||||||
|
// Try token-based lookup first
|
||||||
|
if result, exists := orch.GetResult(token); exists {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"domain": result.EmailDomain,
|
||||||
|
"token": result.ReportToken,
|
||||||
|
"status": result.Status,
|
||||||
|
"subdomains_discovered": len(result.Subdomains),
|
||||||
|
"report": result.ReportFile,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: domain-based lookup (for backward compat / simulation page)
|
||||||
|
// This supports the old polling mechanism where domain is the key
|
||||||
|
domain := c.Param("token") // "token" param could also be a domain
|
||||||
|
if result := orch.GetResultByDomain(domain); result != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"domain": result.EmailDomain,
|
||||||
|
"token": result.ReportToken,
|
||||||
|
"status": result.Status,
|
||||||
|
"subdomains_discovered": len(result.Subdomains),
|
||||||
|
"report": result.ReportFile,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Scan not found"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----- ADMIN: Dashboard -----
|
||||||
|
router.GET("/admin-dashboard", func(c *gin.Context) {
|
||||||
|
results := orch.GetAllResults()
|
||||||
|
total, completed, running := orch.CountStats()
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "admin.html", gin.H{
|
||||||
|
"Results": results,
|
||||||
|
"Total": total,
|
||||||
|
"Completed": completed,
|
||||||
|
"Running": running,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----- EMAIL: Send report via SMTP -----
|
||||||
|
router.POST("/email-report", func(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no email provided in payload, try to get it from the scan result
|
||||||
|
sendTo := req.Email
|
||||||
|
if sendTo == "" {
|
||||||
|
result, exists := orch.GetResult(req.Token)
|
||||||
|
if !exists {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Report not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendTo = result.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
if !emailRegex.MatchString(sendTo) {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid email address"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := orch.SendEmailReport(req.Token, sendTo); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to send email: %v", err)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a report summary page for admin
|
||||||
|
reportURL := "/reports/" + req.Token + ".html"
|
||||||
|
|
||||||
|
// Generate QR for the admin to scan (use full URL if baseURL is set)
|
||||||
|
qrReportURL := reportURL
|
||||||
|
if baseURL != "" {
|
||||||
|
qrReportURL = baseURL + reportURL
|
||||||
|
}
|
||||||
|
qrData := ""
|
||||||
|
png, qrErr := qrcode.Encode(qrReportURL, qrcode.Medium, 256)
|
||||||
|
if qrErr == nil {
|
||||||
|
qrData = "/reports/qr_" + req.Token + ".png"
|
||||||
|
os.WriteFile(filepath.Join(projectDir, "reports", "qr_" + req.Token + ".png"), png, 0644)
|
||||||
|
}
|
||||||
|
_ = qrData
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "Report sent successfully",
|
||||||
|
"report": reportURL,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----- SIMULATION: Serve new simulation page with token support -----
|
||||||
|
router.StaticFile("/simulation", "./src/static/simulation.html")
|
||||||
|
|
||||||
|
// ----- DOWNLOAD: Serve the raw scan results for debugging -----
|
||||||
|
router.GET("/report-data/:token", func(c *gin.Context) {
|
||||||
|
token := c.Param("token")
|
||||||
|
result, exists := orch.GetResult(token)
|
||||||
|
if !exists {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Report not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----- API: List all scan tokens (for admin integration) -----
|
||||||
|
router.GET("/api/scans", func(c *gin.Context) {
|
||||||
|
results := orch.GetAllResults()
|
||||||
|
type scanSummary struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Time string `json:"created_at"`
|
||||||
|
}
|
||||||
|
summaries := make([]scanSummary, 0, len(results))
|
||||||
|
for _, r := range results {
|
||||||
|
summaries = append(summaries, scanSummary{
|
||||||
|
Token: r.ReportToken,
|
||||||
|
Domain: r.EmailDomain,
|
||||||
|
Status: string(r.Status),
|
||||||
|
Time: r.CreatedAt.Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, summaries)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----- Signal handling + graceful shutdown -----
|
||||||
|
signalChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: "0.0.0.0:8080",
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
fmt.Printf("AASD server starting on %s\n", srv.Addr)
|
||||||
|
fmt.Printf("Version: 2026-04.1 | API Attack Surface Discovery\n")
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
fmt.Printf("Server error: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-signalChan
|
||||||
|
fmt.Println("\nShutdown signal received, shutting down gracefully...")
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer shutdownCancel()
|
||||||
|
|
||||||
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||||
|
fmt.Printf("Server shutdown error: %v\n", err)
|
||||||
|
}
|
||||||
|
fmt.Println("AASD server stopped")
|
||||||
|
}
|
||||||
281
AttackSurface/src/cmd/resilience-challenge/main.go
Normal file
281
AttackSurface/src/cmd/resilience-challenge/main.go
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/skip2/go-qrcode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScanResult holds the result of a GoTestWAF scan
|
||||||
|
type ScanResult struct {
|
||||||
|
EmailDomain string
|
||||||
|
ReportFile string
|
||||||
|
Status string // "pending", "running", "completed", "failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// in-memory storage for scan results
|
||||||
|
scanResults = make(map[string]*ScanResult)
|
||||||
|
mu sync.RWMutex
|
||||||
|
// emailRegex validates basic email format
|
||||||
|
emailRegex = regexp.MustCompile(`^[^@]+@[^@]+\.[^@]+$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create main context for graceful shutdown
|
||||||
|
mainCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
router := gin.Default()
|
||||||
|
router.LoadHTMLGlob("src/templates/*")
|
||||||
|
|
||||||
|
// Serve static files (frontend)
|
||||||
|
router.Static("/static", "./src/static")
|
||||||
|
router.StaticFile("/", "./src/static/index.html")
|
||||||
|
router.StaticFile("/simulation", "./src/static/simulation.html")
|
||||||
|
// Serve GoTestWAF reports
|
||||||
|
router.Static("/reports", "./reports")
|
||||||
|
|
||||||
|
// QR code generator endpoint
|
||||||
|
router.GET("/qrcode", func(c *gin.Context) {
|
||||||
|
text := c.Query("text")
|
||||||
|
if text == "" {
|
||||||
|
text = "WX-2026"
|
||||||
|
}
|
||||||
|
png, err := qrcode.Encode(text, qrcode.Medium, 256)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate QR code"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Data(http.StatusOK, "image/png", png)
|
||||||
|
})
|
||||||
|
|
||||||
|
// API endpoint: start security scan
|
||||||
|
router.POST("/start", func(c *gin.Context) {
|
||||||
|
email := strings.TrimSpace(c.PostForm("email"))
|
||||||
|
if email == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Email is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate email format
|
||||||
|
if !emailRegex.MatchString(email) {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid email format"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract domain from email
|
||||||
|
parts := strings.Split(email, "@")
|
||||||
|
domain := parts[1] // safe because regex ensures exactly one @
|
||||||
|
|
||||||
|
// Domain validation
|
||||||
|
if !strings.Contains(domain, ".") {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid domain"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent path traversal in domain (no slashes or backslashes)
|
||||||
|
if strings.ContainsAny(domain, "/\\") {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid domain characters"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit domain length
|
||||||
|
if len(domain) > 255 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Domain too long"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store scan result (pending)
|
||||||
|
mu.Lock()
|
||||||
|
scanResults[domain] = &ScanResult{
|
||||||
|
EmailDomain: domain,
|
||||||
|
ReportFile: fmt.Sprintf("report_%s.html", strings.ReplaceAll(domain, ".", "_")),
|
||||||
|
Status: "pending",
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
// Trigger background scan (goroutine)
|
||||||
|
go startGoTestWAFScan(mainCtx, domain)
|
||||||
|
|
||||||
|
// Redirect to simulation page with domain as query param
|
||||||
|
c.Redirect(http.StatusFound, "/simulation?domain="+domain)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Admin dashboard: list all scan results
|
||||||
|
router.GET("/admin-dashboard", func(c *gin.Context) {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
|
||||||
|
results := make([]ScanResult, 0, len(scanResults))
|
||||||
|
completed := 0
|
||||||
|
running := 0
|
||||||
|
for _, r := range scanResults {
|
||||||
|
results = append(results, *r)
|
||||||
|
switch r.Status {
|
||||||
|
case "completed":
|
||||||
|
completed++
|
||||||
|
case "running":
|
||||||
|
running++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "admin.html", gin.H{
|
||||||
|
"Results": results,
|
||||||
|
"Total": len(results),
|
||||||
|
"Completed": completed,
|
||||||
|
"Running": running,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Endpoint to check scan status (for frontend polling)
|
||||||
|
router.GET("/scan-status/:domain", func(c *gin.Context) {
|
||||||
|
domain := c.Param("domain")
|
||||||
|
mu.RLock()
|
||||||
|
result, exists := scanResults[domain]
|
||||||
|
mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Domain not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"domain": result.EmailDomain,
|
||||||
|
"status": result.Status,
|
||||||
|
"report": result.ReportFile,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle OS signals for graceful shutdown
|
||||||
|
signalChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
// Run server in goroutine
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: "0.0.0.0:8080",
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
fmt.Printf("Server starting on %s\n", srv.Addr)
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
fmt.Printf("Server error: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for shutdown signal
|
||||||
|
<-signalChan
|
||||||
|
fmt.Println("Shutdown signal received, shutting down gracefully...")
|
||||||
|
|
||||||
|
// Cancel main context (signals all goroutines)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
// Create shutdown context with timeout
|
||||||
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer shutdownCancel()
|
||||||
|
|
||||||
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||||
|
fmt.Printf("Server shutdown error: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Server stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
// startGoTestWAFScan launches actual GoTestWAF binary in background
|
||||||
|
func startGoTestWAFScan(parentCtx context.Context, domain string) {
|
||||||
|
mu.Lock()
|
||||||
|
if result, exists := scanResults[domain]; exists {
|
||||||
|
result.Status = "running"
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
// Determine project root (AttackSurface/) relative to current working directory
|
||||||
|
// Assuming the Go program is run from AttackSurface directory (where bin/ and reports/ are)
|
||||||
|
projectRoot := "."
|
||||||
|
reportsDir := filepath.Join(projectRoot, "reports")
|
||||||
|
if err := os.MkdirAll(reportsDir, 0755); err != nil {
|
||||||
|
fmt.Printf("Failed to create reports directory: %v\n", err)
|
||||||
|
updateScanStatus(domain, "failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare GoTestWAF command
|
||||||
|
// Use the actual target server with Wallarm filtering node in monitoring mode
|
||||||
|
targetURL := "https://git.sechpoint.app"
|
||||||
|
binaryPath := filepath.Join(projectRoot, "bin", "gotestwaf")
|
||||||
|
|
||||||
|
// Verify binary exists
|
||||||
|
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
|
||||||
|
fmt.Printf("GoTestWAF binary not found at %s\n", binaryPath)
|
||||||
|
updateScanStatus(domain, "failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create context with 120-second timeout for GoTestWAF scan
|
||||||
|
scanCtx, cancel := context.WithTimeout(parentCtx, 120*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(scanCtx, binaryPath,
|
||||||
|
"--url", targetURL,
|
||||||
|
"--configPath", "src/gotestwaf/config.yaml",
|
||||||
|
"--maxIdleConns", "2",
|
||||||
|
"--maxRedirects", "10",
|
||||||
|
"--reportPath", reportsDir,
|
||||||
|
"--reportName", fmt.Sprintf("report_%s", strings.ReplaceAll(domain, ".", "_")),
|
||||||
|
"--reportFormat", "html",
|
||||||
|
"--wafName", "generic",
|
||||||
|
"--skipWAFBlockCheck",
|
||||||
|
"--nonBlockedAsPassed",
|
||||||
|
"--tlsVerify",
|
||||||
|
"--noEmailReport",
|
||||||
|
"--quiet",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set working directory to project root (AttackSurface/)
|
||||||
|
cmd.Dir = projectRoot
|
||||||
|
|
||||||
|
// Run command in goroutine
|
||||||
|
go func() {
|
||||||
|
fmt.Printf("Starting GoTestWAF scan for domain: %s (target: %s)\n", domain, targetURL)
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
// Check if error is due to timeout
|
||||||
|
if scanCtx.Err() == context.DeadlineExceeded {
|
||||||
|
fmt.Printf("GoTestWAF scan timed out after 120s for domain: %s\n", domain)
|
||||||
|
updateScanStatus(domain, "failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("GoTestWAF scan failed for %s: %v\nOutput:\n%s\n", domain, err, output)
|
||||||
|
updateScanStatus(domain, "failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed := time.Since(startTime)
|
||||||
|
fmt.Printf("GoTestWAF scan completed for %s in %v\n", domain, elapsed)
|
||||||
|
updateScanStatus(domain, "completed")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateScanStatus updates the scan result status
|
||||||
|
func updateScanStatus(domain string, status string) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if result, exists := scanResults[domain]; exists {
|
||||||
|
result.Status = status
|
||||||
|
}
|
||||||
|
}
|
||||||
40
AttackSurface/src/go.mod
Normal file
40
AttackSurface/src/go.mod
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
module aasd
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.12.0
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
|
golang.org/x/net v0.51.0 // indirect
|
||||||
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
|
golang.org/x/text v0.34.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
|
)
|
||||||
91
AttackSurface/src/go.sum
Normal file
91
AttackSurface/src/go.sum
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
|
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
|
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
|
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
14
AttackSurface/src/gotestwaf/.dockerignore
Normal file
14
AttackSurface/src/gotestwaf/.dockerignore
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
**/
|
||||||
|
|
||||||
|
# build image
|
||||||
|
!.git/
|
||||||
|
!cmd/
|
||||||
|
!internal/
|
||||||
|
!pkg/
|
||||||
|
!vendor/
|
||||||
|
!go.mod
|
||||||
|
!go.sum
|
||||||
|
|
||||||
|
# result image
|
||||||
|
!testcases/
|
||||||
|
!config.yaml
|
||||||
2
AttackSurface/src/gotestwaf/.gitattributes
vendored
Normal file
2
AttackSurface/src/gotestwaf/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
vendor/** linguist-vendored
|
||||||
|
**/*.pb.go linguist-generated
|
||||||
35
AttackSurface/src/gotestwaf/.github/workflows/dockerhub-description.yml
vendored
Normal file
35
AttackSurface/src/gotestwaf/.github/workflows/dockerhub-description.yml
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
name: Update DockerHub Description
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-description:
|
||||||
|
runs-on: self-hosted-amd64-1cpu
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Import secrets
|
||||||
|
uses: hashicorp/vault-action@v3
|
||||||
|
id: secrets
|
||||||
|
with:
|
||||||
|
exportEnv: false
|
||||||
|
url: ${{ secrets.VAULT_URL }}
|
||||||
|
role: ${{ secrets.VAULT_ROLE }}
|
||||||
|
method: kubernetes
|
||||||
|
path: kubernetes-ci
|
||||||
|
secrets: |
|
||||||
|
kv-gitlab-ci/data/github/shared/dockerhub-creds user | DOCKERHUB_USER ;
|
||||||
|
kv-gitlab-ci/data/github/shared/dockerhub-creds password | DOCKERHUB_PASSWORD ;
|
||||||
|
|
||||||
|
- name: Update DockerHub Description
|
||||||
|
uses: peter-evans/dockerhub-description@v4
|
||||||
|
with:
|
||||||
|
username: ${{ steps.secrets.outputs.DOCKERHUB_USER }}
|
||||||
|
password: ${{ steps.secrets.outputs.DOCKERHUB_PASSWORD }}
|
||||||
|
repository: wallarm/gotestwaf
|
||||||
|
short-description: ${{ github.event.repository.description }}
|
||||||
|
readme-filepath: ./README.md
|
||||||
109
AttackSurface/src/gotestwaf/.github/workflows/dockerhub-push.yml
vendored
Normal file
109
AttackSurface/src/gotestwaf/.github/workflows/dockerhub-push.yml
vendored
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
name: Build Docker image and push to DockerHub
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: self-hosted-amd64-1cpu
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Import secrets
|
||||||
|
uses: hashicorp/vault-action@v3
|
||||||
|
id: secrets
|
||||||
|
with:
|
||||||
|
exportEnv: false
|
||||||
|
url: ${{ secrets.VAULT_URL }}
|
||||||
|
role: ${{ secrets.VAULT_ROLE }}
|
||||||
|
method: kubernetes
|
||||||
|
path: kubernetes-ci
|
||||||
|
secrets: |
|
||||||
|
kv-gitlab-ci/data/github/shared/dockerhub-creds user | DOCKERHUB_USER ;
|
||||||
|
kv-gitlab-ci/data/github/shared/dockerhub-creds password | DOCKERHUB_PASSWORD ;
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ steps.secrets.outputs.DOCKERHUB_USER }}
|
||||||
|
password: ${{ steps.secrets.outputs.DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: wallarm/gotestwaf
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=raw,value=edge,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
clean-old-cache:
|
||||||
|
needs: build-and-push
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Delete old cached docker layers
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
LAST_WORKFLOW_TIME=$(
|
||||||
|
gh run list \
|
||||||
|
--json workflowName,startedAt \
|
||||||
|
--jq ".[] | select( .workflowName == \"${{ github.workflow }}\") | .startedAt" \
|
||||||
|
| head -n 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Time of the last running '${{ github.workflow }}' workflow: $LAST_WORKFLOW_TIME"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
OLD_CACHE_IDS=$(
|
||||||
|
gh api \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
--jq ".actions_caches[] | select(.last_accessed_at < \"$LAST_WORKFLOW_TIME\") | .id" \
|
||||||
|
/repos/wallarm/gotestwaf/actions/caches \
|
||||||
|
| tr '\n' ' '
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ -z "$OLD_CACHE_IDS" ]; then
|
||||||
|
echo "Done"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ID of caches to delete: $OLD_CACHE_IDS"
|
||||||
|
|
||||||
|
for cache_id in $OLD_CACHE_IDS; do
|
||||||
|
gh api \
|
||||||
|
--method DELETE \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
/repos/wallarm/gotestwaf/actions/caches/$cache_id
|
||||||
|
done
|
||||||
|
done
|
||||||
7
AttackSurface/src/gotestwaf/.gitignore
vendored
Normal file
7
AttackSurface/src/gotestwaf/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
.idea
|
||||||
|
.tmp
|
||||||
|
.DS_store
|
||||||
|
/gotestwaf
|
||||||
|
/main
|
||||||
|
/reports/*
|
||||||
|
/modsec_stat_*.txt
|
||||||
153
AttackSurface/src/gotestwaf/.golangci.yaml
Normal file
153
AttackSurface/src/gotestwaf/.golangci.yaml
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
linters-settings:
|
||||||
|
depguard:
|
||||||
|
list-type: blacklist
|
||||||
|
packages:
|
||||||
|
# logging is allowed only by logutils.Log, logrus
|
||||||
|
# is allowed to use only in logutils package
|
||||||
|
- github.com/sirupsen/logrus
|
||||||
|
packages-with-error-message:
|
||||||
|
- github.com/sirupsen/logrus: "logging is allowed only by logutils.Log"
|
||||||
|
dupl:
|
||||||
|
threshold: 100
|
||||||
|
funlen:
|
||||||
|
lines: 100
|
||||||
|
statements: 50
|
||||||
|
gci:
|
||||||
|
local-prefixes: github.com/golangci/golangci-lint
|
||||||
|
goconst:
|
||||||
|
min-len: 2
|
||||||
|
min-occurrences: 2
|
||||||
|
gocritic:
|
||||||
|
enabled-tags:
|
||||||
|
- diagnostic
|
||||||
|
- experimental
|
||||||
|
- opinionated
|
||||||
|
- performance
|
||||||
|
- style
|
||||||
|
disabled-checks:
|
||||||
|
- dupImport # https://github.com/go-critic/go-critic/issues/845
|
||||||
|
- ifElseChain
|
||||||
|
- octalLiteral
|
||||||
|
- whyNoLint
|
||||||
|
- wrapperFunc
|
||||||
|
gocyclo:
|
||||||
|
min-complexity: 15
|
||||||
|
goimports:
|
||||||
|
local-prefixes: github.com/golangci/golangci-lint
|
||||||
|
golint:
|
||||||
|
min-confidence: 0
|
||||||
|
gomnd:
|
||||||
|
settings:
|
||||||
|
mnd:
|
||||||
|
# don't include the "operation" and "assign"
|
||||||
|
checks: argument,case,condition,return
|
||||||
|
govet:
|
||||||
|
check-shadowing: true
|
||||||
|
settings:
|
||||||
|
printf:
|
||||||
|
funcs:
|
||||||
|
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
|
||||||
|
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
|
||||||
|
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
|
||||||
|
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
|
||||||
|
lll:
|
||||||
|
line-length: 140
|
||||||
|
maligned:
|
||||||
|
suggest-new: true
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
nolintlint:
|
||||||
|
allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space)
|
||||||
|
allow-unused: false # report any unused nolint directives
|
||||||
|
require-explanation: false # don't require an explanation for nolint directives
|
||||||
|
require-specific: false # don't require nolint directives to be specific about which linter is being skipped
|
||||||
|
|
||||||
|
linters:
|
||||||
|
# please, do not use `enable-all`: it's deprecated and will be removed soon.
|
||||||
|
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
- bodyclose
|
||||||
|
- deadcode
|
||||||
|
- depguard
|
||||||
|
- dogsled
|
||||||
|
- dupl
|
||||||
|
- errcheck
|
||||||
|
- exhaustive
|
||||||
|
#- funlen
|
||||||
|
- gochecknoinits
|
||||||
|
- goconst
|
||||||
|
- gocritic
|
||||||
|
- gocyclo
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
- golint
|
||||||
|
- gomnd
|
||||||
|
- goprintffuncname
|
||||||
|
- gosec
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- interfacer
|
||||||
|
- lll
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- noctx
|
||||||
|
- nolintlint
|
||||||
|
- rowserrcheck
|
||||||
|
- scopelint
|
||||||
|
- staticcheck
|
||||||
|
- structcheck
|
||||||
|
- stylecheck
|
||||||
|
- typecheck
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- unused
|
||||||
|
- varcheck
|
||||||
|
- whitespace
|
||||||
|
|
||||||
|
# don't enable:
|
||||||
|
# - asciicheck
|
||||||
|
# - gochecknoglobals
|
||||||
|
# - gocognit
|
||||||
|
# - godot
|
||||||
|
# - godox
|
||||||
|
# - goerr113
|
||||||
|
# - maligned
|
||||||
|
# - nestif
|
||||||
|
# - prealloc
|
||||||
|
# - testpackage
|
||||||
|
# - wsl
|
||||||
|
|
||||||
|
issues:
|
||||||
|
# Excluding configuration per-path, per-linter, per-text and per-source
|
||||||
|
exclude-rules:
|
||||||
|
- path: _test\.go
|
||||||
|
linters:
|
||||||
|
- gomnd
|
||||||
|
|
||||||
|
# https://github.com/go-critic/go-critic/issues/926
|
||||||
|
- linters:
|
||||||
|
- gocritic
|
||||||
|
text: "unnecessaryDefer:"
|
||||||
|
|
||||||
|
# TODO temporary rule, must be removed
|
||||||
|
# seems related to v0.34.1, but I was not able to reproduce locally,
|
||||||
|
# I was also not able to reproduce in the CI of a fork,
|
||||||
|
# only the golangci-lint CI seems to be affected by this invalid analysis.
|
||||||
|
- path: pkg/golinters/scopelint.go
|
||||||
|
text: 'directive `//nolint:interfacer` is unused for linter interfacer'
|
||||||
|
|
||||||
|
run:
|
||||||
|
skip-dirs:
|
||||||
|
- test/testdata_etc
|
||||||
|
- internal/cache
|
||||||
|
- internal/renameio
|
||||||
|
- internal/robustio
|
||||||
|
|
||||||
|
# golangci.com configuration
|
||||||
|
# https://github.com/golangci/golangci/wiki/Configuration
|
||||||
|
service:
|
||||||
|
golangci-lint-version: 1.23.x # use the fixed version to not introduce new linters unexpectedly
|
||||||
|
prepare:
|
||||||
|
- echo "here I can run custom commands, but no preparation needed for this repo"
|
||||||
55
AttackSurface/src/gotestwaf/Dockerfile
Normal file
55
AttackSurface/src/gotestwaf/Dockerfile
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
# Build Stage ==================================================================
|
||||||
|
FROM golang:1.24-alpine AS build
|
||||||
|
|
||||||
|
RUN apk --no-cache add git
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY ./go.mod ./go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN go build -o gotestwaf \
|
||||||
|
-ldflags "-X github.com/wallarm/gotestwaf/internal/version.Version=$(git describe --tags)" \
|
||||||
|
./cmd/gotestwaf
|
||||||
|
|
||||||
|
|
||||||
|
# Main Stage ===================================================================
|
||||||
|
FROM alpine
|
||||||
|
|
||||||
|
# Prepare environment
|
||||||
|
RUN <<EOF
|
||||||
|
set -e -o pipefail
|
||||||
|
|
||||||
|
# install all dependencies
|
||||||
|
apk add --no-cache \
|
||||||
|
tini \
|
||||||
|
chromium \
|
||||||
|
font-inter \
|
||||||
|
fontconfig
|
||||||
|
|
||||||
|
fc-cache -fv
|
||||||
|
|
||||||
|
# add non-root user
|
||||||
|
addgroup gtw
|
||||||
|
adduser -D -G gtw gtw
|
||||||
|
|
||||||
|
# create dir for application
|
||||||
|
mkdir /app
|
||||||
|
mkdir /app/reports
|
||||||
|
chown -R gtw:gtw /app
|
||||||
|
EOF
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=build /app/gotestwaf ./
|
||||||
|
COPY ./testcases ./testcases
|
||||||
|
COPY ./config.yaml ./
|
||||||
|
|
||||||
|
USER gtw
|
||||||
|
|
||||||
|
VOLUME [ "/app/reports" ]
|
||||||
|
|
||||||
|
ENTRYPOINT [ "/sbin/tini", "--", "/app/gotestwaf" ]
|
||||||
21
AttackSurface/src/gotestwaf/LICENSE
Normal file
21
AttackSurface/src/gotestwaf/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Wallarm
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
63
AttackSurface/src/gotestwaf/Makefile
Normal file
63
AttackSurface/src/gotestwaf/Makefile
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
GOTESTWAF_VERSION := $(shell git describe --tags)
|
||||||
|
|
||||||
|
gotestwaf:
|
||||||
|
DOCKER_BUILDKIT=1 docker build --force-rm -t gotestwaf .
|
||||||
|
|
||||||
|
gotestwaf_bin:
|
||||||
|
go build -o gotestwaf \
|
||||||
|
-ldflags "-X github.com/wallarm/gotestwaf/internal/version.Version=$(GOTESTWAF_VERSION)" \
|
||||||
|
./cmd/gotestwaf
|
||||||
|
|
||||||
|
modsec:
|
||||||
|
docker pull mendhak/http-https-echo:31
|
||||||
|
docker run --rm -d --name gotestwaf_test_app -p 8088:8080 mendhak/http-https-echo:31
|
||||||
|
docker pull owasp/modsecurity-crs:nginx-alpine
|
||||||
|
docker run --rm -d --name gotestwaf_modsec -p 8080:8080 -p 8443:8443 \
|
||||||
|
-e BACKEND="http://$$(docker inspect --format '{{.NetworkSettings.IPAddress}}' gotestwaf_test_app):8080" \
|
||||||
|
-e PARANOIA=1 \
|
||||||
|
owasp/modsecurity-crs:nginx-alpine
|
||||||
|
|
||||||
|
modsec_down:
|
||||||
|
docker kill gotestwaf_test_app gotestwaf_modsec
|
||||||
|
|
||||||
|
modsec_stat: gotestwaf
|
||||||
|
./misc/modsec_stat.sh
|
||||||
|
|
||||||
|
scan_local:
|
||||||
|
go run ./cmd --url=http://127.0.0.1:8080/ --workers 200 --noEmailReport
|
||||||
|
|
||||||
|
scan_local_from_docker:
|
||||||
|
docker run --rm -v ${PWD}/reports:/app/reports --network="host" \
|
||||||
|
gotestwaf --url=http://127.0.0.1:8080/ --workers 200 --noEmailReport
|
||||||
|
|
||||||
|
modsec_crs_regression_tests_convert:
|
||||||
|
rm -rf .tmp/coreruleset
|
||||||
|
rm -rf testcases/modsec-crs/
|
||||||
|
rm -rf testcases/modsec-crs-false-pos/
|
||||||
|
git clone --depth 1 https://github.com/coreruleset/coreruleset .tmp/coreruleset
|
||||||
|
ruby misc/modsec_regression_testset_converter.rb
|
||||||
|
mkdir testcases/modsec-crs-false-pos
|
||||||
|
mv testcases/modsec-crs/fp_* testcases/modsec-crs-false-pos/
|
||||||
|
rm -rf .tmp
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test -count=1 -v ./...
|
||||||
|
|
||||||
|
lint:
|
||||||
|
golangci-lint -v run ./...
|
||||||
|
|
||||||
|
tidy:
|
||||||
|
go mod tidy
|
||||||
|
go mod vendor
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
go fmt $(shell go list ./... | grep -v /vendor/)
|
||||||
|
goimports -local "github.com/wallarm/gotestwaf" -w $(shell find . -type f -name '*.go' -not -name '*_mocks.go' -not -name '*.pb.go' -not -path "./vendor/*")
|
||||||
|
|
||||||
|
|
||||||
|
delete_reports:
|
||||||
|
rm -f ./reports/*.pdf
|
||||||
|
rm -f ./reports/*.csv
|
||||||
|
|
||||||
|
.PHONY: gotestwaf gotestwaf_bin modsec modsec_down scan_local \
|
||||||
|
scan_local_from_docker test lint tidy fmt delete_reports
|
||||||
482
AttackSurface/src/gotestwaf/README.md
Normal file
482
AttackSurface/src/gotestwaf/README.md
Normal file
|
|
@ -0,0 +1,482 @@
|
||||||
|
# GoTestWAF [](https://www.blackhat.com/us-22/arsenal/schedule/index.html#gotestwaf---well-known-open-source-waf-tester-now-supports-api-security-hacking-27986)
|
||||||
|
|
||||||
|
GoTestWAF is a tool for API and OWASP attack simulation that supports a wide range of API protocols including
|
||||||
|
REST, GraphQL, gRPC, SOAP, XMLRPC, and others.
|
||||||
|
|
||||||
|
It was designed to evaluate web application security solutions, such as API security proxies, Web Application Firewalls,
|
||||||
|
IPS, API gateways, and others.
|
||||||
|
|
||||||
|
---
|
||||||
|
* [How it works](#how-it-works)
|
||||||
|
* [Requirements](#requirements)
|
||||||
|
* [Quick start with Docker](#quick-start-with-docker)
|
||||||
|
* [Checking evaluation results](#checking-the-evaluation-results)
|
||||||
|
* [Demos](#demos)
|
||||||
|
* [Other options to run GoTestWAF](#other-options-to-run-gotestwaf)
|
||||||
|
* [Configuration options](#configuration-options)
|
||||||
|
* [Running with OWASP Core Rule Set regression testing suite](#running-with-owasp-core-rule-set-regression-testing-suite)
|
||||||
|
---
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
GoTestWAF generates malicious requests using encoded payloads placed in different parts of HTTP requests: its body, headers,
|
||||||
|
URL parameters, etc. Generated requests are sent to the application security solution URL specified during GoTestWAF launch.
|
||||||
|
The results of the security solution evaluation are recorded in the report file created on your machine.
|
||||||
|
|
||||||
|
Default conditions for request generation are defined in the `testcases` folder in the YAML files of the following format:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
payload:
|
||||||
|
- '"union select -7431.1, name, @aaa from u_base--w-'
|
||||||
|
- "'or 123.22=123.22"
|
||||||
|
- "' waitfor delay '00:00:10'--"
|
||||||
|
- "')) or pg_sleep(5)--"
|
||||||
|
encoder:
|
||||||
|
- Base64Flat
|
||||||
|
- URL
|
||||||
|
placeholder:
|
||||||
|
- UrlPath
|
||||||
|
- UrlParam
|
||||||
|
- JSUnicode
|
||||||
|
- Header
|
||||||
|
type: SQL Injection
|
||||||
|
```
|
||||||
|
|
||||||
|
* `payload` is a malicious attack sample (e.g XSS payload like ```<script>alert(111)</script>``` or something more sophisticated).
|
||||||
|
Since the format of the YAML string is required for payloads, they must be [encoded as binary data](https://yaml.org/type/binary.html).
|
||||||
|
|
||||||
|
* `encoder` is an encoder to be applied to the payload before placing it to the HTTP request. Possible encoders are:
|
||||||
|
|
||||||
|
* Base64
|
||||||
|
* Base64Flat
|
||||||
|
* JSUnicode
|
||||||
|
* URL
|
||||||
|
* Plain (to keep the payload string as-is)
|
||||||
|
* XML Entity
|
||||||
|
|
||||||
|
* `placeholder` is a place inside HTTP request where encoded payload should be. Possible placeholders are:
|
||||||
|
|
||||||
|
* gRPC
|
||||||
|
* Header
|
||||||
|
* UserAgent
|
||||||
|
* RequestBody
|
||||||
|
* JSONRequest
|
||||||
|
* JSONBody
|
||||||
|
* HTMLForm
|
||||||
|
* HTMLMultipartForm
|
||||||
|
* SOAPBody
|
||||||
|
* XMLBody
|
||||||
|
* URLParam
|
||||||
|
* URLPath
|
||||||
|
* RawRequest
|
||||||
|
|
||||||
|
The `RawRequest` placeholder will allow you to do an arbitrary HTTP request. The payload is substituted by replacing the string `{{payload}}` in the URL path, Headers or body. Fields of `RawRequest` placeholder:
|
||||||
|
|
||||||
|
* `method`
|
||||||
|
* `path`
|
||||||
|
* `headers`
|
||||||
|
* `body`
|
||||||
|
|
||||||
|
Required fields for `RawRequest` placeholder:
|
||||||
|
|
||||||
|
* `method` field
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
payload:
|
||||||
|
- test
|
||||||
|
encoder:
|
||||||
|
- Plain
|
||||||
|
placeholder:
|
||||||
|
- RawRequest:
|
||||||
|
method: "POST"
|
||||||
|
path: "/"
|
||||||
|
headers:
|
||||||
|
Content-Type: "multipart/form-data; boundary=boundary"
|
||||||
|
body: |
|
||||||
|
--boundary
|
||||||
|
Content-disposition: form-data; name="field1"
|
||||||
|
|
||||||
|
Test
|
||||||
|
--boundary
|
||||||
|
Content-disposition: form-data; name="field2"
|
||||||
|
Content-Type: text/plain; charset=utf-7
|
||||||
|
|
||||||
|
Knock knock.
|
||||||
|
{{payload}}
|
||||||
|
--boundary--
|
||||||
|
type: RawRequest test
|
||||||
|
```
|
||||||
|
|
||||||
|
* `type` is a name of entire group of the payloads in file. It can be arbitrary, but should reflect the type of attacks in the file.
|
||||||
|
|
||||||
|
Request generation is a three-step process involving the multiplication of payload amount by encoder and placeholder amounts.
|
||||||
|
Let's say you defined 2 **payloads**, 3 **encoders** (Base64, JSUnicode, and URL) and 1 **placeholder** (URLParameter - HTTP GET parameter).
|
||||||
|
In this case, GoTestWAF will send 2x3x1 = 6 requests in a test case.
|
||||||
|
|
||||||
|
During GoTestWAF launch, you can also choose test cases between two embedded: OWASP Top-10, OWASP-API,
|
||||||
|
or your own (by using the [configuration option](#configuration-options) `testCasePath`).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
* GoTestwaf supports all the popular operating systems (Linux, Windows, macOS), and can be built natively
|
||||||
|
if [Go](https://golang.org/doc/install) is installed in the system. If you want to run GoTestWaf natively,
|
||||||
|
make sure you have the Chrome web browser to be able to generate PDF reports. In case you don't have Chrome,
|
||||||
|
you can create a report in HTML format.
|
||||||
|
* If running GoTestWAF as the Docker container, please ensure you have [installed and configured Docker](https://docs.docker.com/get-docker/),
|
||||||
|
and GoTestWAF and evaluated application security solution are connected to the same [Docker network](https://docs.docker.com/network/).
|
||||||
|
* For GoTestWAF to be successfully started, please ensure the IP address of the machine running GoTestWAF is whitelisted
|
||||||
|
on the machine running the application security solution.
|
||||||
|
|
||||||
|
## Quick start with Docker
|
||||||
|
|
||||||
|
The steps below walk through downloading and starting GoTestWAF with minimal configuration on Docker.
|
||||||
|
|
||||||
|
1. Pull the [GoTestWAF image](https://hub.docker.com/r/wallarm/gotestwaf) from Docker Hub:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker pull wallarm/gotestwaf
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start the GoTestWAF image:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run --rm --network="host" -it -v ${PWD}/reports:/app/reports \
|
||||||
|
wallarm/gotestwaf --url=<EVALUATED_SECURITY_SOLUTION_URL>
|
||||||
|
```
|
||||||
|
|
||||||
|
If required, you can replace `${PWD}/reports` with the path to another folder used to place the evaluation report.
|
||||||
|
|
||||||
|
If you don't want to optionally email the report, just press Enter after the email request message appears, or you can use --noEmailReport to skip the message:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run --rm --network="host" -v ${PWD}/reports:/app/reports \
|
||||||
|
wallarm/gotestwaf --url=<EVALUATED_SECURITY_SOLUTION_URL> --noEmailReport
|
||||||
|
```
|
||||||
|
|
||||||
|
If the evaluated security tool is available externally, you can skip the option `--network="host"`. This option enables interaction of Docker containers running on 127.0.0.1.
|
||||||
|
|
||||||
|
To perform the gRPC tests you must have a working endpoint and use the --grpcPort <port> cli option.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run --rm --network="host" -it -v ${PWD}/reports:/app/reports \
|
||||||
|
wallarm/gotestwaf --grpcPort 9000 --url=http://my.grpc.endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Check your email for the report.
|
||||||
|
|
||||||
|
You have successfully evaluated your application security solution by using GoTestWAF with minimal configuration.
|
||||||
|
To learn advanced configuration options, please use this [link](#configuration-options).
|
||||||
|
|
||||||
|
## Checking the evaluation results
|
||||||
|
|
||||||
|
Check the evaluation results logged using the `STDOUT` and `STDERR` services. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO[0000] GoTestWAF started version=v0.5.6-7-g48e6959
|
||||||
|
INFO[0000] Test cases loading started
|
||||||
|
INFO[0000] Test cases loading finished
|
||||||
|
INFO[0000] Test cases fingerprint fp=c6d14d6138601d19d215bb97806bcda3
|
||||||
|
INFO[0000] Try to identify WAF solution
|
||||||
|
INFO[0000] WAF was not identified
|
||||||
|
INFO[0000] gohttp is used as an HTTP client to make requests http_client=gohttp
|
||||||
|
INFO[0000] WAF pre-check url="http://host.docker.internal:8080"
|
||||||
|
INFO[0000] WAF pre-check blocked=true code=403 status=done
|
||||||
|
INFO[0000] gRPC pre-check status=started
|
||||||
|
INFO[0000] gRPC pre-check connection="not available" status=done
|
||||||
|
INFO[0000] GraphQL pre-check status=started
|
||||||
|
INFO[0000] GraphQL pre-check connection="not available" status=done
|
||||||
|
INFO[0000] Scanning started url="http://host.docker.internal:8080"
|
||||||
|
INFO[0005] Scanning finished duration=5.422700876s
|
||||||
|
True-Positive Tests:
|
||||||
|
┌────────────┬───────────────────────────┬──────────────────────┬─────────────────────┬──────────────────────┬────────────────────┬─────────────┬─────────────────┐
|
||||||
|
│ TEST SET │ TEST CASE │ PERCENTAGE , % │ BLOCKED │ BYPASSED │ UNRESOLVED │ SENT │ FAILED │
|
||||||
|
├────────────┼───────────────────────────┼──────────────────────┼─────────────────────┼──────────────────────┼────────────────────┼─────────────┼─────────────────┤
|
||||||
|
│ community │ community-128kb-rce │ 0.00 │ 0 │ 0 │ 1 │ 1 │ 0 │
|
||||||
|
│ community │ community-128kb-sqli │ 0.00 │ 0 │ 0 │ 1 │ 1 │ 0 │
|
||||||
|
│ community │ community-128kb-xss │ 0.00 │ 0 │ 0 │ 1 │ 1 │ 0 │
|
||||||
|
│ community │ community-16kb-rce │ 100.00 │ 1 │ 0 │ 0 │ 1 │ 0 │
|
||||||
|
│ community │ community-16kb-sqli │ 100.00 │ 1 │ 0 │ 0 │ 1 │ 0 │
|
||||||
|
│ community │ community-16kb-xss │ 100.00 │ 1 │ 0 │ 0 │ 1 │ 0 │
|
||||||
|
│ community │ community-32kb-rce │ 100.00 │ 1 │ 0 │ 0 │ 1 │ 0 │
|
||||||
|
│ community │ community-32kb-sqli │ 100.00 │ 1 │ 0 │ 0 │ 1 │ 0 │
|
||||||
|
│ community │ community-32kb-xss │ 100.00 │ 1 │ 0 │ 0 │ 1 │ 0 │
|
||||||
|
│ community │ community-64kb-rce │ 100.00 │ 1 │ 0 │ 0 │ 1 │ 0 │
|
||||||
|
│ community │ community-64kb-sqli │ 100.00 │ 1 │ 0 │ 0 │ 1 │ 0 │
|
||||||
|
│ community │ community-64kb-xss │ 100.00 │ 1 │ 0 │ 0 │ 1 │ 0 │
|
||||||
|
│ community │ community-8kb-rce │ 100.00 │ 1 │ 0 │ 0 │ 1 │ 0 │
|
||||||
|
│ community │ community-8kb-sqli │ 100.00 │ 1 │ 0 │ 0 │ 1 │ 0 │
|
||||||
|
│ community │ community-8kb-xss │ 100.00 │ 1 │ 0 │ 0 │ 1 │ 0 │
|
||||||
|
│ community │ community-lfi │ 100.00 │ 8 │ 0 │ 0 │ 8 │ 0 │
|
||||||
|
│ community │ community-lfi-multipart │ 0.00 │ 0 │ 0 │ 2 │ 2 │ 0 │
|
||||||
|
│ community │ community-rce │ 50.00 │ 2 │ 2 │ 0 │ 4 │ 0 │
|
||||||
|
│ community │ community-rce-rawrequests │ 100.00 │ 3 │ 0 │ 0 │ 3 │ 0 │
|
||||||
|
│ community │ community-sqli │ 100.00 │ 12 │ 0 │ 0 │ 12 │ 0 │
|
||||||
|
│ community │ community-user-agent │ 66.67 │ 6 │ 3 │ 0 │ 9 │ 0 │
|
||||||
|
│ community │ community-xss │ 88.46 │ 92 │ 12 │ 0 │ 104 │ 0 │
|
||||||
|
│ community │ community-xxe │ 0.00 │ 0 │ 1 │ 1 │ 2 │ 0 │
|
||||||
|
│ owasp │ crlf │ 85.71 │ 6 │ 1 │ 0 │ 7 │ 0 │
|
||||||
|
│ owasp │ ldap-injection │ 8.33 │ 2 │ 22 │ 0 │ 24 │ 0 │
|
||||||
|
│ owasp │ mail-injection │ 12.50 │ 3 │ 21 │ 0 │ 24 │ 0 │
|
||||||
|
│ owasp │ nosql-injection │ 24.00 │ 12 │ 38 │ 0 │ 50 │ 0 │
|
||||||
|
│ owasp │ path-traversal │ 30.00 │ 6 │ 14 │ 0 │ 20 │ 0 │
|
||||||
|
│ owasp │ rce │ 33.33 │ 2 │ 4 │ 0 │ 6 │ 0 │
|
||||||
|
│ owasp │ rce-urlparam │ 33.33 │ 3 │ 6 │ 0 │ 9 │ 0 │
|
||||||
|
│ owasp │ rce-urlpath │ 0.00 │ 0 │ 3 │ 0 │ 3 │ 0 │
|
||||||
|
│ owasp │ shell-injection │ 18.75 │ 6 │ 26 │ 0 │ 32 │ 0 │
|
||||||
|
│ owasp │ sql-injection │ 29.17 │ 14 │ 34 │ 0 │ 48 │ 0 │
|
||||||
|
│ owasp │ ss-include │ 50.00 │ 12 │ 12 │ 0 │ 24 │ 0 │
|
||||||
|
│ owasp │ sst-injection │ 29.17 │ 7 │ 17 │ 0 │ 24 │ 0 │
|
||||||
|
│ owasp │ xml-injection │ 0.00 │ 0 │ 7 │ 0 │ 7 │ 0 │
|
||||||
|
│ owasp │ xss-scripting │ 39.91 │ 89 │ 134 │ 1 │ 224 │ 0 │
|
||||||
|
│ owasp-api │ graphql │ 0.00 │ 0 │ 0 │ 0 │ 0 │ 0 │
|
||||||
|
│ owasp-api │ graphql-post │ 0.00 │ 0 │ 0 │ 0 │ 0 │ 0 │
|
||||||
|
│ owasp-api │ grpc │ 0.00 │ 0 │ 0 │ 0 │ 0 │ 0 │
|
||||||
|
│ owasp-api │ non-crud │ 100.00 │ 2 │ 0 │ 0 │ 2 │ 0 │
|
||||||
|
│ owasp-api │ rest │ 42.86 │ 3 │ 4 │ 0 │ 7 │ 0 │
|
||||||
|
│ owasp-api │ soap │ 20.00 │ 1 │ 4 │ 0 │ 5 │ 0 │
|
||||||
|
├────────────┼───────────────────────────┼──────────────────────┼─────────────────────┼──────────────────────┼────────────────────┼─────────────┼─────────────────┤
|
||||||
|
│ Date: │ Project Name: │ True-Positive Score: │ Blocked (Resolved): │ Bypassed (Resolved): │ Unresolved (Sent): │ Total Sent: │ Failed (Total): │
|
||||||
|
│ 2025-07-14 │ generic │ 45.36% │ 303/668 (45.36%) │ 365/668 (54.64%) │ 7/675 (1.04%) │ 675 │ 0/675 (0.00%) │
|
||||||
|
└────────────┴───────────────────────────┴──────────────────────┴─────────────────────┴──────────────────────┴────────────────────┴─────────────┴─────────────────┘
|
||||||
|
|
||||||
|
True-Negative Tests:
|
||||||
|
┌────────────┬───────────────┬──────────────────────┬─────────────────────┬──────────────────────┬────────────────────┬─────────────┬─────────────────┐
|
||||||
|
│ TEST SET │ TEST CASE │ PERCENTAGE , % │ BLOCKED │ BYPASSED │ UNRESOLVED │ SENT │ FAILED │
|
||||||
|
├────────────┼───────────────┼──────────────────────┼─────────────────────┼──────────────────────┼────────────────────┼─────────────┼─────────────────┤
|
||||||
|
│ false-pos │ texts │ 90.78 │ 13 │ 128 │ 0 │ 141 │ 0 │
|
||||||
|
├────────────┼───────────────┼──────────────────────┼─────────────────────┼──────────────────────┼────────────────────┼─────────────┼─────────────────┤
|
||||||
|
│ Date: │ Project Name: │ True-Negative Score: │ Blocked (Resolved): │ Bypassed (Resolved): │ Unresolved (Sent): │ Total Sent: │ Failed (Total): │
|
||||||
|
│ 2025-07-14 │ generic │ 90.78% │ 13/141 (9.22%) │ 128/141 (90.78%) │ 0/141 (0.00%) │ 141 │ 0/141 (0.00%) │
|
||||||
|
└────────────┴───────────────┴──────────────────────┴─────────────────────┴──────────────────────┴────────────────────┴─────────────┴─────────────────┘
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
┌──────────────────────┬───────────────────────────────┬──────────────────────────────┬─────────┐
|
||||||
|
│ TYPE │ TRUE - POSITIVE TESTS BLOCKED │ TRUE - NEGATIVE TESTS PASSED │ AVERAGE │
|
||||||
|
├──────────────────────┼───────────────────────────────┼──────────────────────────────┼─────────┤
|
||||||
|
│ API Security │ 42.86% │ n/a │ 42.86% │
|
||||||
|
│ Application Security │ 45.41% │ 90.78% │ 68.10% │
|
||||||
|
├──────────────────────┼───────────────────────────────┼──────────────────────────────┼─────────┤
|
||||||
|
│ │ │ Score │ 55.48% │
|
||||||
|
└──────────────────────┴───────────────────────────────┴──────────────────────────────┴─────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
The report file `waf-evaluation-report-<date>.pdf` is available in the `reports` folder of the user directory. You can also specify the directory to save the reports with the `reportPath` parameter and the name of the report file with the `reportName` parameter. To learn advanced configuration options, please use this [link](#configuration-options).
|
||||||
|
|
||||||
|
You can found an example of PDF report [here](./docs/report_example.pdf).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Demos
|
||||||
|
|
||||||
|
You can try GoTestWAF by running the demo environment that deploys NGINX‑based [ModSecurity using OWASP Core Rule Set](https://hub.docker.com/r/owasp/modsecurity-crs)
|
||||||
|
and GoTestWAF evaluating ModSecurity on Docker.
|
||||||
|
|
||||||
|
To run the demo environment:
|
||||||
|
|
||||||
|
1. Clone this repository and go to the cloned directory:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/wallarm/gotestwaf.git
|
||||||
|
cd gotestwaf
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start ModSecurity from the [Docker image](https://hub.docker.com/r/owasp/modsecurity-crs/) by using the following `make` command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make modsec
|
||||||
|
```
|
||||||
|
|
||||||
|
Settings for running the ModSecurity Docker container are defined in the rule `modsec` of the cloned Makefile. It runs the ModSecurity Docker container on port 8080 with minimal configuration defined in the cloned file `./resources/default.conf.template` and the `PARANOIA` value set to 1.
|
||||||
|
|
||||||
|
If required, you can change these settings by editing the rule `modsec` in the cloned Makefile. Available options for ModSecurity configuration are described on [Docker Hub](https://hub.docker.com/r/owasp/modsecurity-crs/).
|
||||||
|
|
||||||
|
To stop ModSecurity containers use the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make modsec_down
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Start GoTestWAF with minimal configuration by using one of the following methods:
|
||||||
|
|
||||||
|
Start the [Docker image](https://hub.docker.com/r/wallarm/gotestwaf) by using the following `docker pull` and `docker run` commands:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker pull wallarm/gotestwaf
|
||||||
|
docker run --rm --network="host" -v ${PWD}/reports:/app/reports \
|
||||||
|
wallarm/gotestwaf --url=http://127.0.0.1:8080 --noEmailReport
|
||||||
|
```
|
||||||
|
|
||||||
|
Build the GoTestWAF Docker image from the [Dockerfile](./Dockerfile) and run the
|
||||||
|
image by using the following `make` commands (make sure ModSec is running on port 8080; if not, update the port value in the Makefile):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make gotestwaf
|
||||||
|
make scan_local_from_docker
|
||||||
|
```
|
||||||
|
|
||||||
|
Start GoTestWAF natively with go by using the following `make` command:
|
||||||
|
(make sure ModSec is running on port 8080; if not, update the port value in the Makefile):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make scan_local
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Find the [report](#checking-the-evaluation-results) file `waf-evaluation-report-<date>.pdf` in
|
||||||
|
the `reports` folder that you mapped to `/app/reports` inside the container.
|
||||||
|
|
||||||
|
## Other options to run GoTestWAF
|
||||||
|
|
||||||
|
In addition to running the GoTestWAF Docker image downloaded from Docker Hub, you can run GoTestWAF by using the following options:
|
||||||
|
|
||||||
|
* Clone this repository and build the GoTestWAF Docker image from the [Dockerfile](./Dockerfile), for example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/wallarm/gotestwaf.git
|
||||||
|
cd gotestwaf
|
||||||
|
DOCKER_BUILDKIT=1 docker build --force-rm -t gotestwaf .
|
||||||
|
docker run --rm --network="host" -it -v ${PWD}/reports:/app/reports \
|
||||||
|
gotestwaf --url=<EVALUATED_SECURITY_SOLUTION_URL>
|
||||||
|
```
|
||||||
|
|
||||||
|
If the evaluated security tool is available externally, you can skip the option `--network="host"`. This option enables interaction of Docker containers running on 127.0.0.1.
|
||||||
|
|
||||||
|
* Clone this repository and run GoTestWAF with [`go`](https://golang.org/doc/), for example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/wallarm/gotestwaf.git
|
||||||
|
cd gotestwaf
|
||||||
|
go run ./cmd --url=<EVALUATED_SECURITY_SOLUTION_URL>
|
||||||
|
```
|
||||||
|
|
||||||
|
* Clone this repository and build GoTestWAF as the Go module:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/wallarm/gotestwaf.git
|
||||||
|
cd gotestwaf
|
||||||
|
go build -mod vendor -o gotestwaf ./cmd
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported GoTestWAF configuration options are described below.
|
||||||
|
|
||||||
|
## Configuration options
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage: ./gotestwaf [OPTIONS] --url <URL>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--addDebugHeader Add header "X-GoTestWAF-Test" with a hash of the test information in each request
|
||||||
|
--addHeader string An HTTP header to add to requests
|
||||||
|
--blockConnReset If present, connection resets will be considered as block
|
||||||
|
--blockRegex string Regex to detect a blocking page with the same HTTP response status code as a not blocked request
|
||||||
|
--blockStatusCodes ints HTTP status code that WAF uses while blocking requests (default [403])
|
||||||
|
--configPath string Path to the config file (default "config.yaml")
|
||||||
|
--email string E-mail to which the report will be sent
|
||||||
|
--followCookies If present, use cookies sent by the server. May work only with --maxIdleConns=1 (gohttp only)
|
||||||
|
--graphqlURL string GraphQL URL to check
|
||||||
|
--grpcPort uint16 gRPC port to check
|
||||||
|
--hideArgsInReport If present, GoTestWAF CLI arguments will not be displayed in the report
|
||||||
|
--httpClient string Which HTTP client use to send requests: chrome, gohttp (default "gohttp")
|
||||||
|
--idleConnTimeout int The maximum amount of time a keep-alive connection will live (gohttp only) (default 2)
|
||||||
|
--ignoreUnresolved If present, unresolved test cases will be considered as bypassed (affect score and results)
|
||||||
|
--includePayloads If present, payloads will be included in HTML/PDF report
|
||||||
|
--logFormat string Set logging format: text, json (default "text")
|
||||||
|
--logLevel string Logging level: panic, fatal, error, warn, info, debug, trace (default "info")
|
||||||
|
--maxIdleConns int The maximum number of keep-alive connections (gohttp only) (default 2)
|
||||||
|
--maxRedirects int The maximum number of handling redirects (gohttp only) (default 50)
|
||||||
|
--noEmailReport Save report locally
|
||||||
|
--nonBlockedAsPassed If present, count requests that weren't blocked as passed. If false, requests that don't satisfy to PassStatusCodes/PassRegExp as blocked
|
||||||
|
--openapiFile string Path to openAPI file
|
||||||
|
--passRegex string Regex to a detect normal (not blocked) web page with the same HTTP status code as a blocked request
|
||||||
|
--passStatusCodes ints HTTP response status code that WAF uses while passing requests (default [200,404])
|
||||||
|
--proxy string Proxy URL to use
|
||||||
|
--quiet If present, disable verbose logging
|
||||||
|
--randomDelay int Random delay in ms in addition to the delay between requests (default 400)
|
||||||
|
--renewSession Renew cookies before each test. Should be used with --followCookies flag (gohttp only)
|
||||||
|
--reportFormat strings Export report in the following formats: none, json, html, pdf (default [pdf])
|
||||||
|
--reportName string Report file name. Supports `time' package template format (default "waf-evaluation-report-2006-January-02-15-04-05")
|
||||||
|
--reportPath string A directory to store reports (default "reports")
|
||||||
|
--sendDelay int Delay in ms between requests (default 400)
|
||||||
|
--skipWAFBlockCheck If present, WAF detection tests will be skipped
|
||||||
|
--skipWAFIdentification Skip WAF identification
|
||||||
|
--testCase string If set then only this test case will be run
|
||||||
|
--testCasesPath string Path to a folder with test cases (default "testcases")
|
||||||
|
--testSet string If set then only this test set's cases will be run
|
||||||
|
--tlsVerify If present, the received TLS certificate will be verified
|
||||||
|
--url string URL to check
|
||||||
|
--version Show GoTestWAF version and exit
|
||||||
|
--wafName string Name of the WAF product (default "generic")
|
||||||
|
--workers int The number of workers to scan (default 5)
|
||||||
|
```
|
||||||
|
|
||||||
|
GoTestWAF supports two HTTP clients for performing requests, selectable via the `--httpClient` option. The default client is the standard Golang HTTP client. The second option is Chrome, which can be used with the `--httpClient=chrome` CLI argument. Note that on Linux systems, you must add the `--cap-add=SYS_ADMIN` argument to the Docker arguments to run GoTestWAF with Chrome as the request performer.
|
||||||
|
|
||||||
|
|
||||||
|
### Scan based on OpenAPI file
|
||||||
|
|
||||||
|
For better scanning, GTW supports sending malicious vectors through valid application requests. Instead of constructing requests that are simple in structure and send them to the URL specified at startup, GoTestWAF creates valid requests based on the application's API description in the OpenAPI 3.0 format.
|
||||||
|
|
||||||
|
How it works:
|
||||||
|
|
||||||
|
1. GoTestWAF loads an OpenAPI file and constructs request templates. All templates are then divided into groups based on what placeholders they support (e.g., if there is a string parameter in the request path, then such a request will be assigned to a group of requests that support URLPath placeholder)
|
||||||
|
|
||||||
|
2. The next malicious vector is selected from the queue for sending. Based on the placeholder specified for it, all query templates are selected into which this vector can be substituted. Next, the vector is substituted into template and the request is sent.
|
||||||
|
|
||||||
|
3. Based on the possible responses specified in the OpenAPI file, it is determined whether the request was blocked by WAF or passed to the application. If the status of the response code and its scheme match those described in the OpenAPI file, the request is marked as bypassed. Otherwise, it will be marked as blocked. It is possible that the application only responds with a status code, and this status code matches the response from the WAF. In this case, the request will be marked as unresolved.
|
||||||
|
|
||||||
|
Some supported OpenAPI features:
|
||||||
|
|
||||||
|
* numeric and string parameters in headers, paths, query parameters and body of requests;
|
||||||
|
|
||||||
|
* the following content-types are supported for the request body: `application/json`, `application/xml`, `application/x-www-form-urlencoded`, `text/plain`;
|
||||||
|
|
||||||
|
* the following modifiers are supported for XML: `name`, `wrapped`, `attribute`, `prefix`, `namespace`;
|
||||||
|
|
||||||
|
* length limits for strings are supported through the `minLength` and `maxLength` parameters;
|
||||||
|
|
||||||
|
* value restrictions for numbers are supported through `minimum`, `maximum`, `exclusiveMinimum` and `exclusiveMaximum`;
|
||||||
|
|
||||||
|
* restrictions on the length of arrays through `minItems` and `maxItems` are supported;
|
||||||
|
|
||||||
|
* combination of schemes via `oneOf`, `anyOf`, `allOf` is supported.
|
||||||
|
|
||||||
|
Based on the described principle of operation, it is extremely important that the OpenAPI file correctly represents the implemented application API. Therefore, for example, you cannot use `default` to describe possible responses to queries.
|
||||||
|
|
||||||
|
Note: You need to forward volume with openapi spec to GoTestWAF container.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
-v ${PWD}/api.yaml:/app/api.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Complete Docker Example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run --rm --network="host" -it -v ${PWD}/reports:/app/reports -v ${PWD}/api.yaml:/app/api.yaml wallarm/gotestwaf --wafName your_waf_name --url=https://example.com/v1 --openapiFile api.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running with OWASP Core Rule Set regression testing suite
|
||||||
|
|
||||||
|
GoTestWAF allows easy integration of additional test suites.
|
||||||
|
|
||||||
|
In this example, we will demonstrate how to add tests from the OWASP Core Rule Set regression testing suite.
|
||||||
|
|
||||||
|
Since the tests are written in a different format than the GoTestWAF format, a conversion is required. For this purpose, the script **misc/modsec_regression_testset_converter.rb** is provided.
|
||||||
|
|
||||||
|
To convert the tests, run `make modsec_crs_regression_tests_convert`.
|
||||||
|
Then, build a container with the updated set of tests.
|
||||||
|
`make gotestwaf`
|
||||||
|
|
||||||
|
Note that by default, tests are converted for only a subset of rules. The following categories have been chosen:
|
||||||
|
|
||||||
|
- REQUEST-932-APPLICATION-ATTACK-RCE
|
||||||
|
- REQUEST-933-APPLICATION-ATTACK-PHP
|
||||||
|
- REQUEST-941-APPLICATION-ATTACK-XSS
|
||||||
|
- REQUEST-930-APPLICATION-ATTACK-LFI
|
||||||
|
- REQUEST-931-APPLICATION-ATTACK-RFI
|
||||||
|
- REQUEST-942-APPLICATION-ATTACK-SQLI
|
||||||
|
- REQUEST-944-APPLICATION-ATTACK-JAVA
|
||||||
|
- REQUEST-934-APPLICATION-ATTACK-GENERIC
|
||||||
|
- REQUEST-913-SCANNER-DETECTION
|
||||||
|
|
||||||
|
If needed, modify the variable "crs_testcases" in misc/modsec_regression_testset_converter.rb to add or remove test categories.
|
||||||
349
AttackSurface/src/gotestwaf/cmd/gotestwaf/flags.go
Normal file
349
AttackSurface/src/gotestwaf/cmd/gotestwaf/flags.go
Normal file
|
|
@ -0,0 +1,349 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/config"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/helpers"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/report"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
textLogFormat = "text"
|
||||||
|
jsonLogFormat = "json"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logFormatsSet = map[string]any{
|
||||||
|
textLogFormat: nil,
|
||||||
|
jsonLogFormat: nil,
|
||||||
|
}
|
||||||
|
logFormats = slices.Collect(maps.Keys(logFormatsSet))
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
chromeClient = "chrome"
|
||||||
|
gohttpClient = "gohttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
httpClientsSet = map[string]any{
|
||||||
|
chromeClient: nil,
|
||||||
|
gohttpClient: nil,
|
||||||
|
}
|
||||||
|
httpClients = slices.Collect(maps.Keys(httpClientsSet))
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxReportFilenameLength = 249 // 255 (max length) - 5 (".html") - 1 (to be sure)
|
||||||
|
|
||||||
|
defaultReportPath = "reports"
|
||||||
|
defaultReportName = "waf-evaluation-report-2006-January-02-15-04-05"
|
||||||
|
defaultTestCasesPath = "testcases"
|
||||||
|
defaultConfigPath = "config.yaml"
|
||||||
|
|
||||||
|
wafName = "generic"
|
||||||
|
)
|
||||||
|
|
||||||
|
const cliDescription = `GoTestWAF is a tool for API and OWASP attack simulation that supports a
|
||||||
|
wide range of API protocols including REST, GraphQL, gRPC, SOAP, XMLRPC, and others.
|
||||||
|
Homepage: https://github.com/wallarm/gotestwaf
|
||||||
|
|
||||||
|
Usage: %s [OPTIONS] --url <URL>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
`
|
||||||
|
|
||||||
|
var (
|
||||||
|
configPath string
|
||||||
|
quiet bool
|
||||||
|
logLevel logrus.Level
|
||||||
|
logFormat string
|
||||||
|
|
||||||
|
isIncludePayloadsFlagUsed bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var usage = func() {
|
||||||
|
flag.CommandLine.SetOutput(os.Stdout)
|
||||||
|
usage := cliDescription
|
||||||
|
fmt.Fprintf(os.Stdout, usage, os.Args[0])
|
||||||
|
flag.PrintDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFlags parses all GoTestWAF CLI flags
|
||||||
|
func parseFlags() (args []string, err error) {
|
||||||
|
reportPath := filepath.Join(".", defaultReportPath)
|
||||||
|
testCasesPath := filepath.Join(".", defaultTestCasesPath)
|
||||||
|
|
||||||
|
flag.Usage = usage
|
||||||
|
|
||||||
|
// General parameters
|
||||||
|
flag.StringVar(&configPath, "configPath", defaultConfigPath, "Path to the config file")
|
||||||
|
flag.BoolVar(&quiet, "quiet", false, "If present, disable verbose logging")
|
||||||
|
logLvl := flag.String("logLevel", "info", "Logging level: panic, fatal, error, warn, info, debug, trace")
|
||||||
|
flag.StringVar(&logFormat, "logFormat", textLogFormat, "Set logging format: "+strings.Join(logFormats, ", "))
|
||||||
|
showVersion := flag.Bool("version", false, "Show GoTestWAF version and exit")
|
||||||
|
|
||||||
|
// Target settings
|
||||||
|
urlParam := flag.String("url", "", "URL to check")
|
||||||
|
flag.Uint16("grpcPort", 0, "gRPC port to check")
|
||||||
|
graphqlURL := flag.String("graphqlURL", "", "GraphQL URL to check")
|
||||||
|
openapiFile := flag.String("openapiFile", "", "Path to openAPI file")
|
||||||
|
|
||||||
|
// Test cases settings
|
||||||
|
flag.String("testCase", "", "If set then only this test case will be run")
|
||||||
|
flag.String("testCasesPath", testCasesPath, "Path to a folder with test cases")
|
||||||
|
flag.String("testSet", "", "If set then only this test set's cases will be run")
|
||||||
|
|
||||||
|
// HTTP client settings
|
||||||
|
httpClient := flag.String("httpClient", gohttpClient, "Which HTTP client use to send requests: "+strings.Join(httpClients, ", "))
|
||||||
|
flag.Bool("tlsVerify", false, "If present, the received TLS certificate will be verified")
|
||||||
|
flag.String("proxy", "", "Proxy URL to use")
|
||||||
|
flag.String("addHeader", "", "An HTTP header to add to requests")
|
||||||
|
flag.Bool("addDebugHeader", false, "Add header \"X-GoTestWAF-Test\" with a hash of the test information in each request")
|
||||||
|
|
||||||
|
// GoHTTP client only settings
|
||||||
|
flag.Int("maxIdleConns", 2, "The maximum number of keep-alive connections (gohttp only)")
|
||||||
|
flag.Int("maxRedirects", 50, "The maximum number of handling redirects (gohttp only)")
|
||||||
|
flag.Int("idleConnTimeout", 2, "The maximum amount of time a keep-alive connection will live (gohttp only)")
|
||||||
|
flag.Bool("followCookies", false, "If present, use cookies sent by the server. May work only with --maxIdleConns=1 (gohttp only)")
|
||||||
|
flag.Bool("renewSession", false, "Renew cookies before each test. Should be used with --followCookies flag (gohttp only)")
|
||||||
|
|
||||||
|
// Performance settings
|
||||||
|
flag.Int("workers", 5, "The number of workers to scan")
|
||||||
|
flag.Int("sendDelay", 400, "Delay in ms between requests")
|
||||||
|
flag.Int("randomDelay", 400, "Random delay in ms in addition to the delay between requests")
|
||||||
|
|
||||||
|
// Analysis settings
|
||||||
|
flag.Bool("skipWAFBlockCheck", false, "If present, WAF detection tests will be skipped")
|
||||||
|
flag.Bool("skipWAFIdentification", false, "Skip WAF identification")
|
||||||
|
flag.IntSlice("blockStatusCodes", []int{403}, "HTTP status code that WAF uses while blocking requests")
|
||||||
|
flag.IntSlice("passStatusCodes", []int{200, 404}, "HTTP response status code that WAF uses while passing requests")
|
||||||
|
blockRegex := flag.String("blockRegex", "",
|
||||||
|
"Regex to detect a blocking page with the same HTTP response status code as a not blocked request")
|
||||||
|
passRegex := flag.String("passRegex", "",
|
||||||
|
"Regex to a detect normal (not blocked) web page with the same HTTP status code as a blocked request")
|
||||||
|
flag.Bool("nonBlockedAsPassed", false,
|
||||||
|
"If present, count requests that weren't blocked as passed. If false, requests that don't satisfy to PassStatusCodes/PassRegExp as blocked")
|
||||||
|
flag.Bool("ignoreUnresolved", false, "If present, unresolved test cases will be considered as bypassed (affect score and results)")
|
||||||
|
flag.Bool("blockConnReset", false, "If present, connection resets will be considered as block")
|
||||||
|
|
||||||
|
// Report settings
|
||||||
|
flag.String("wafName", wafName, "Name of the WAF product")
|
||||||
|
flag.Bool("includePayloads", false, "If present, payloads will be included in HTML/PDF report")
|
||||||
|
flag.String("reportPath", reportPath, "A directory to store reports")
|
||||||
|
reportName := flag.String("reportName", defaultReportName, "Report file name. Supports `time' package template format")
|
||||||
|
reportFormat := flag.StringSlice("reportFormat", []string{report.PdfFormat}, "Export report in the following formats: "+strings.Join(report.ReportFormats, ", "))
|
||||||
|
noEmailReport := flag.Bool("noEmailReport", false, "Save report locally")
|
||||||
|
email := flag.String("email", "", "E-mail to which the report will be sent")
|
||||||
|
flag.Bool("hideArgsInReport", false, "If present, GoTestWAF CLI arguments will not be displayed in the report")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if len(os.Args) == 1 {
|
||||||
|
usage()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// show version and exit
|
||||||
|
if *showVersion == true {
|
||||||
|
fmt.Fprintf(os.Stderr, "GoTestWAF %s\n", version.Version)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// url flag must be set
|
||||||
|
if *urlParam == "" {
|
||||||
|
return nil, errors.New("--url flag is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !terminal.IsTerminal(int(os.Stdin.Fd())) {
|
||||||
|
if *noEmailReport == false && *email == "" {
|
||||||
|
return nil, errors.New(
|
||||||
|
"GoTestWAF is running in a non-interactive session. " +
|
||||||
|
"Please use the '-it' flag if you are running GTW in Docker or use the " +
|
||||||
|
"'--email' (or '--noEmailReport') and '--includePayloads' ('true' or 'false') options",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *noEmailReport == false && *email != "" {
|
||||||
|
*email, err = helpers.ValidateEmail(*email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "couldn't validate email")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logrusLogLvl, err := logrus.ParseLevel(*logLvl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
logLevel = logrusLogLvl
|
||||||
|
|
||||||
|
if err = validateLogFormat(logFormat); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = validateHttpClient(*httpClient); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = report.ValidateReportFormat(*reportFormat); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
validURL, err := validateURL(*urlParam, httpProto)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "URL is not valid")
|
||||||
|
}
|
||||||
|
*urlParam = validURL.String()
|
||||||
|
|
||||||
|
// format GraphQL URL from given HTTP URL
|
||||||
|
gqlValidURL, err := checkOrCraftProtocolURL(*graphqlURL, *urlParam, graphqlProto)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "graphqlURL is not valid")
|
||||||
|
}
|
||||||
|
*graphqlURL = gqlValidURL.String()
|
||||||
|
|
||||||
|
// Force GoHTTP to be used as the HTTP client
|
||||||
|
// when scanning against the OpenAPI spec.
|
||||||
|
if openapiFile != nil && len(*openapiFile) > 0 {
|
||||||
|
*httpClient = "gohttp"
|
||||||
|
}
|
||||||
|
|
||||||
|
if *blockRegex != "" {
|
||||||
|
_, err = regexp.Compile(*blockRegex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "bad regexp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *passRegex != "" {
|
||||||
|
_, err = regexp.Compile(*passRegex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "bad regexp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, reportFileName := filepath.Split(*reportName)
|
||||||
|
if len(reportFileName) > maxReportFilenameLength {
|
||||||
|
return nil, errors.New("report filename too long")
|
||||||
|
}
|
||||||
|
|
||||||
|
checkUsedFlags()
|
||||||
|
|
||||||
|
args, err = normalizeArgs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "couldn't normalize args")
|
||||||
|
}
|
||||||
|
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkUsedFlags() {
|
||||||
|
fn := func(f *flag.Flag) {
|
||||||
|
if f.Name == "includePayloads" {
|
||||||
|
isIncludePayloadsFlagUsed = f.Changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flag.Visit(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeArgs returns string with used CLI args in a unified from.
|
||||||
|
func normalizeArgs() ([]string, error) {
|
||||||
|
// disable lexicographical order
|
||||||
|
flag.CommandLine.SortFlags = false
|
||||||
|
|
||||||
|
var (
|
||||||
|
args []string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
fn := func(f *flag.Flag) {
|
||||||
|
// skip if flag wasn't changed
|
||||||
|
if !f.Changed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
value string
|
||||||
|
arg string
|
||||||
|
)
|
||||||
|
|
||||||
|
// all types listed in parseFlags function
|
||||||
|
argType := f.Value.Type()
|
||||||
|
switch argType {
|
||||||
|
case "string":
|
||||||
|
value = strings.TrimSpace(f.Value.String())
|
||||||
|
|
||||||
|
if strings.Contains(value, " ") {
|
||||||
|
value = `"` + value + `"`
|
||||||
|
}
|
||||||
|
|
||||||
|
arg = fmt.Sprintf("--%s=%s", f.Name, value)
|
||||||
|
|
||||||
|
case "stringSlice":
|
||||||
|
// remove square brackets: [pdf,json] -> pdf,json
|
||||||
|
value = strings.Trim(f.Value.String(), "[]")
|
||||||
|
arg = fmt.Sprintf("--%s=%s", f.Name, value)
|
||||||
|
|
||||||
|
case "bool":
|
||||||
|
arg = fmt.Sprintf("--%s", f.Name)
|
||||||
|
|
||||||
|
case "int", "uint16":
|
||||||
|
value = f.Value.String()
|
||||||
|
arg = fmt.Sprintf("--%s=%s", f.Name, value)
|
||||||
|
|
||||||
|
case "intSlice":
|
||||||
|
// remove square brackets: [200,404] -> 200,404
|
||||||
|
value = strings.Trim(f.Value.String(), "[]")
|
||||||
|
arg = fmt.Sprintf("--%s=%s", f.Name, value)
|
||||||
|
|
||||||
|
default:
|
||||||
|
err = multierror.Append(err, fmt.Errorf("unknown CLI argument type: %s", argType))
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all changed flags
|
||||||
|
flag.Visit(fn)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadConfig loads the specified config file and merges it with the parameters passed via CLI
|
||||||
|
func loadConfig() (cfg *config.Config, err error) {
|
||||||
|
err = viper.BindPFlags(flag.CommandLine)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
viper.AddConfigPath(".")
|
||||||
|
viper.SetConfigFile(configPath)
|
||||||
|
viper.AutomaticEnv()
|
||||||
|
|
||||||
|
err = viper.ReadInConfig()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = viper.Unmarshal(&cfg)
|
||||||
|
return
|
||||||
|
}
|
||||||
83
AttackSurface/src/gotestwaf/cmd/gotestwaf/helpers.go
Normal file
83
AttackSurface/src/gotestwaf/cmd/gotestwaf/helpers.go
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
httpProto = "http"
|
||||||
|
graphqlProto = httpProto
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidScheme = errors.New("invalid URL scheme")
|
||||||
|
ErrEmptyHost = errors.New("empty host")
|
||||||
|
)
|
||||||
|
|
||||||
|
// validateURL validates the given URL and URL scheme.
|
||||||
|
func validateURL(rawURL string, protocol string) (*url.URL, error) {
|
||||||
|
validURL, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
re := regexp.MustCompile(fmt.Sprintf("^%ss?$", protocol))
|
||||||
|
|
||||||
|
if !re.MatchString(validURL.Scheme) {
|
||||||
|
return nil, ErrInvalidScheme
|
||||||
|
}
|
||||||
|
|
||||||
|
if validURL.Host == "" {
|
||||||
|
return nil, ErrEmptyHost
|
||||||
|
}
|
||||||
|
|
||||||
|
return validURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkOrCraftProtocolURL creates a URL from validHttpURL if the rawURL is empty
|
||||||
|
// or validates the rawURL.
|
||||||
|
func checkOrCraftProtocolURL(rawURL string, validHttpURL string, protocol string) (*url.URL, error) {
|
||||||
|
if rawURL != "" {
|
||||||
|
validURL, err := validateURL(rawURL, protocol)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return validURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
validURL, err := validateURL(validHttpURL, httpProto)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
scheme := protocol
|
||||||
|
|
||||||
|
if validURL.Scheme == "https" {
|
||||||
|
scheme += "s"
|
||||||
|
}
|
||||||
|
|
||||||
|
validURL.Scheme = scheme
|
||||||
|
validURL.Path = ""
|
||||||
|
|
||||||
|
return validURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateHttpClient(httpClient string) error {
|
||||||
|
if _, ok := httpClientsSet[httpClient]; !ok {
|
||||||
|
return fmt.Errorf("invalid HTTP client: %s", httpClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateLogFormat(logFormat string) error {
|
||||||
|
if _, ok := logFormatsSet[logFormat]; !ok {
|
||||||
|
return fmt.Errorf("invalid log format: %s", logFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
287
AttackSurface/src/gotestwaf/cmd/gotestwaf/main.go
Normal file
287
AttackSurface/src/gotestwaf/cmd/gotestwaf/main.go
Normal file
|
|
@ -0,0 +1,287 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/config"
|
||||||
|
|
||||||
|
"github.com/getkin/kin-openapi/openapi3"
|
||||||
|
"github.com/getkin/kin-openapi/routers"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/db"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/helpers"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/openapi"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/report"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/waf_detector"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
logger := logrus.New()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
shutdown := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
sig := <-shutdown
|
||||||
|
logger.WithField("signal", sig).Info("scan canceled")
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
args, err := parseFlags()
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Error("couldn't parse flags")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.SetLevel(logLevel)
|
||||||
|
if logFormat == jsonLogFormat {
|
||||||
|
logger.SetFormatter(&logrus.JSONFormatter{})
|
||||||
|
}
|
||||||
|
if quiet {
|
||||||
|
logger.SetOutput(io.Discard)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := loadConfig()
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Error("couldn't load config")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.HideArgsInReport {
|
||||||
|
cfg.Args = args
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := run(ctx, cfg, logger); err != nil {
|
||||||
|
logger.WithError(err).Error("caught error in main function")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(ctx context.Context, cfg *config.Config, logger *logrus.Logger) error {
|
||||||
|
logger.WithField("version", version.Version).Info("GoTestWAF started")
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var router routers.Router
|
||||||
|
var templates openapi.Templates
|
||||||
|
|
||||||
|
if cfg.OpenAPIFile != "" {
|
||||||
|
var openapiDoc *openapi3.T
|
||||||
|
|
||||||
|
openapiDoc, router, err = openapi.LoadOpenAPISpec(ctx, cfg.OpenAPIFile)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't load OpenAPI spec")
|
||||||
|
}
|
||||||
|
openapiDoc.Servers = append(openapiDoc.Servers, &openapi3.Server{
|
||||||
|
URL: cfg.URL,
|
||||||
|
})
|
||||||
|
|
||||||
|
templates, err = openapi.NewTemplates(openapiDoc, cfg.URL)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't create templates from OpenAPI file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Test cases loading started")
|
||||||
|
|
||||||
|
testCases, err := db.LoadTestCases(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "loading test case")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Test cases loading finished")
|
||||||
|
|
||||||
|
db, err := db.NewDB(testCases)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't create test cases DB")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.WithField("fp", db.Hash).Info("Test cases fingerprint")
|
||||||
|
|
||||||
|
if !cfg.SkipWAFIdentification {
|
||||||
|
detector, err := waf_detector.NewWAFDetector(logger, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't create WAF waf_detector")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Try to identify WAF solution")
|
||||||
|
|
||||||
|
name, vendor, checkFunc, err := detector.DetectWAF(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't detect")
|
||||||
|
}
|
||||||
|
|
||||||
|
if name != "" && vendor != "" {
|
||||||
|
logger.WithFields(logrus.Fields{
|
||||||
|
"solution": name,
|
||||||
|
"vendor": vendor,
|
||||||
|
}).Info("WAF was identified. Force enabling `--followCookies' and `--renewSession' options")
|
||||||
|
|
||||||
|
cfg.CheckBlockFunc = checkFunc
|
||||||
|
cfg.FollowCookies = true
|
||||||
|
cfg.RenewSession = true
|
||||||
|
cfg.WAFName = fmt.Sprintf("%s (%s)", name, vendor)
|
||||||
|
} else {
|
||||||
|
logger.Info("WAF was not identified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.WithField("http_client", cfg.HTTPClient).
|
||||||
|
Infof("%s is used as an HTTP client to make requests", cfg.HTTPClient)
|
||||||
|
|
||||||
|
s, err := scanner.New(logger, cfg, db, templates, router, cfg.AddDebugHeader)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't create scanner")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.HTTPClient != "chrome" {
|
||||||
|
isJsReuqired, err := s.CheckIfJavaScriptRequired(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't check if JavaScript is required to interact with the endpoint")
|
||||||
|
}
|
||||||
|
|
||||||
|
if isJsReuqired {
|
||||||
|
return errors.New("JavaScript is required to interact with the endpoint")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.SkipWAFBlockCheck {
|
||||||
|
err = s.WAFBlockCheck(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.WithField("status", "skipped").Info("WAF pre-check")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.CheckGRPCAvailability(ctx)
|
||||||
|
s.CheckGraphQLAvailability(ctx)
|
||||||
|
|
||||||
|
err = s.Run(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "error occurred while scanning")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = os.Stat(cfg.ReportPath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
if makeErr := os.Mkdir(cfg.ReportPath, 0700); makeErr != nil {
|
||||||
|
return errors.Wrap(makeErr, "creating dir")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reportTime := time.Now()
|
||||||
|
|
||||||
|
// Set report name as entered
|
||||||
|
reportName := cfg.ReportName
|
||||||
|
|
||||||
|
// If report name is default - replace time tokens with real data
|
||||||
|
if cfg.ReportName == defaultReportName {
|
||||||
|
reportName = reportTime.Format(cfg.ReportName)
|
||||||
|
}
|
||||||
|
|
||||||
|
reportFile := filepath.Join(cfg.ReportPath, reportName)
|
||||||
|
|
||||||
|
stat := db.GetStatistics(cfg.IgnoreUnresolved, cfg.NonBlockedAsPassed)
|
||||||
|
|
||||||
|
err = report.RenderConsoleReport(stat, reportTime, cfg.WAFName, cfg.URL, cfg.Args, cfg.IgnoreUnresolved, logFormat)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if report.IsNoneReportFormat(cfg.ReportFormat) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
includePayloads := cfg.IncludePayloads
|
||||||
|
if report.IsPdfOrHtmlReportFormat(cfg.ReportFormat) {
|
||||||
|
askForPayloads := true
|
||||||
|
|
||||||
|
// If the cfg.IncludePayloads is already explicitly set by the user OR
|
||||||
|
// the user has explicitly chosen not to send email report, or has
|
||||||
|
// provided the email to send the report to (which we interpret as
|
||||||
|
// non-interactive mode), do not ask to include the payloads in the report.
|
||||||
|
if isIncludePayloadsFlagUsed || cfg.NoEmailReport || cfg.Email != "" {
|
||||||
|
askForPayloads = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if askForPayloads {
|
||||||
|
input := ""
|
||||||
|
fmt.Print("Do you want to include payload details to the report? ([y/N]): ")
|
||||||
|
fmt.Scanln(&input)
|
||||||
|
|
||||||
|
if strings.TrimSpace(input) == "y" {
|
||||||
|
includePayloads = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reportFiles, err := report.ExportFullReport(
|
||||||
|
ctx, stat, reportFile,
|
||||||
|
reportTime, cfg.WAFName, cfg.URL, cfg.OpenAPIFile, cfg.Args,
|
||||||
|
cfg.IgnoreUnresolved, includePayloads, cfg.ReportFormat,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't export full report")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range reportFiles {
|
||||||
|
reportExt := strings.ToUpper(strings.Trim(filepath.Ext(file), "."))
|
||||||
|
logger.WithField("filename", file).Infof("Export %s full report", reportExt)
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadFiles := filepath.Join(cfg.ReportPath, reportName+".csv")
|
||||||
|
err = db.ExportPayloads(payloadFiles)
|
||||||
|
if err != nil {
|
||||||
|
errors.Wrap(err, "payloads exporting")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.NoEmailReport {
|
||||||
|
email := ""
|
||||||
|
|
||||||
|
if cfg.Email != "" {
|
||||||
|
email = cfg.Email
|
||||||
|
} else {
|
||||||
|
fmt.Print("Email to send the report (ENTER to skip): ")
|
||||||
|
fmt.Scanln(&email)
|
||||||
|
|
||||||
|
email = strings.TrimSpace(email)
|
||||||
|
if email == "" {
|
||||||
|
logger.Info("Skip report sending to email")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
email, err = helpers.ValidateEmail(email)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't validate email")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = report.SendReportByEmail(
|
||||||
|
ctx, stat, email,
|
||||||
|
reportTime, cfg.WAFName, cfg.URL, cfg.OpenAPIFile, cfg.Args,
|
||||||
|
cfg.IgnoreUnresolved, includePayloads,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't send report by email")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.WithField("email", email).Info("The report has been sent to the specified email")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
BIN
AttackSurface/src/gotestwaf/docs/report_example.pdf
Normal file
BIN
AttackSurface/src/gotestwaf/docs/report_example.pdf
Normal file
Binary file not shown.
BIN
AttackSurface/src/gotestwaf/docs/report_preview.png
Normal file
BIN
AttackSurface/src/gotestwaf/docs/report_preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 313 KiB |
71
AttackSurface/src/gotestwaf/go.mod
Normal file
71
AttackSurface/src/gotestwaf/go.mod
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
module github.com/wallarm/gotestwaf
|
||||||
|
|
||||||
|
go 1.24
|
||||||
|
|
||||||
|
toolchain go1.24.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20250706212322-41fb261d0659
|
||||||
|
github.com/chromedp/chromedp v0.13.7
|
||||||
|
github.com/clbanning/mxj v1.8.4
|
||||||
|
github.com/getkin/kin-openapi v0.132.0
|
||||||
|
github.com/go-echarts/go-echarts/v2 v2.2.5
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1
|
||||||
|
github.com/leanovate/gopter v0.2.11
|
||||||
|
github.com/mcnijman/go-emailaddress v1.1.1
|
||||||
|
github.com/olekukonko/tablewriter v1.0.8
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/schollz/progressbar/v3 v3.18.0
|
||||||
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
github.com/spf13/pflag v1.0.6
|
||||||
|
github.com/spf13/viper v1.20.1
|
||||||
|
golang.org/x/crypto v0.39.0
|
||||||
|
golang.org/x/net v0.41.0
|
||||||
|
golang.org/x/text v0.26.0
|
||||||
|
google.golang.org/grpc v1.73.0
|
||||||
|
google.golang.org/protobuf v1.36.6
|
||||||
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||||
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250626171732-1a886bd29d1b // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||||
|
github.com/go-openapi/swag v0.23.1 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
|
||||||
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
|
github.com/gobwas/ws v1.4.0 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||||
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||||
|
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
|
||||||
|
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
|
||||||
|
github.com/olekukonko/errors v1.1.0 // indirect
|
||||||
|
github.com/olekukonko/ll v0.0.9 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/afero v1.14.0 // indirect
|
||||||
|
github.com/spf13/cast v1.9.2 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
|
golang.org/x/term v0.32.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
775
AttackSurface/src/gotestwaf/go.sum
Normal file
775
AttackSurface/src/gotestwaf/go.sum
Normal file
|
|
@ -0,0 +1,775 @@
|
||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||||
|
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||||
|
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||||
|
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||||
|
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||||
|
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||||
|
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||||
|
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||||
|
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||||
|
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||||
|
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||||
|
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||||
|
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||||
|
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||||
|
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||||
|
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
|
||||||
|
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
|
||||||
|
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
|
||||||
|
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||||
|
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||||
|
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||||
|
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||||
|
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||||
|
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||||
|
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||||
|
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||||
|
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||||
|
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||||
|
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||||
|
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||||
|
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||||
|
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||||
|
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||||
|
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||||
|
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||||
|
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||||
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||||
|
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||||
|
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||||
|
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
|
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||||
|
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
|
||||||
|
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20250706212322-41fb261d0659 h1:uyvNf582Z4mmNhVjS4JrXLjkIeYec5viQaEN7rN2XA8=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20250706212322-41fb261d0659/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
|
||||||
|
github.com/chromedp/chromedp v0.13.7 h1:vt+mslxscyvUr58eC+6DLSeeo74jpV/HI2nWetjv/W4=
|
||||||
|
github.com/chromedp/chromedp v0.13.7/go.mod h1:h8GPP6ZtLMLsU8zFbTcb7ZDGCvCy8j/vRoFmRltQx9A=
|
||||||
|
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
|
||||||
|
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||||
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
|
github.com/cinar/indicator v1.2.24/go.mod h1:5eX8f1PG9g3RKSoHsoQxKd8bIN97Cf/gbgxXjihROpI=
|
||||||
|
github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
|
||||||
|
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
|
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
|
github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
|
||||||
|
github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/go-echarts/go-echarts/v2 v2.2.5 h1:Jl0gtQa9i/iTZHEsmzf89HoxX2WTGa4K5r0be4qaquE=
|
||||||
|
github.com/go-echarts/go-echarts/v2 v2.2.5/go.mod h1:IN5P8jIRZKENmAJf2lHXBzv8U9YwdVnY9urdzGkEDA0=
|
||||||
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250626171732-1a886bd29d1b h1:ooF9/NzXkXL3OOLRwtPuQT/D7Kx2S5w/Kl1GnMF9h2s=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250626171732-1a886bd29d1b/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||||
|
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||||
|
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||||
|
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||||
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||||
|
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||||
|
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||||
|
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||||
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
|
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
|
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
|
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
|
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
|
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
||||||
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||||
|
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||||
|
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||||
|
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||||
|
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||||
|
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||||
|
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||||
|
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||||
|
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||||
|
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||||
|
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
|
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
|
||||||
|
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
|
||||||
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||||
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||||
|
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||||
|
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mcnijman/go-emailaddress v1.1.1 h1:AGhgVDG3tCDaL0/Vc6erlPQjDuDN3dAT7rRdgFtetr0=
|
||||||
|
github.com/mcnijman/go-emailaddress v1.1.1/go.mod h1:5whZrhS8Xp5LxO8zOD35BC+b76kROtsh+dPomeRt/II=
|
||||||
|
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
|
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||||
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||||
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||||
|
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||||
|
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||||
|
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||||
|
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||||
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||||
|
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||||
|
github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||||
|
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
||||||
|
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
|
||||||
|
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
|
||||||
|
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
|
||||||
|
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||||
|
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||||
|
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
|
||||||
|
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
||||||
|
github.com/olekukonko/tablewriter v1.0.8 h1:f6wJzHg4QUtJdvrVPKco4QTrAylgaU0+b9br/lJxEiQ=
|
||||||
|
github.com/olekukonko/tablewriter v1.0.8/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
|
||||||
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||||
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||||
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
|
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||||
|
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
|
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
||||||
|
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
||||||
|
github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
|
||||||
|
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
||||||
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||||
|
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||||
|
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||||
|
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
|
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
|
||||||
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
|
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||||
|
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||||
|
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||||
|
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
||||||
|
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
|
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
|
||||||
|
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
|
||||||
|
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||||
|
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||||
|
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||||
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
||||||
|
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||||
|
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
|
||||||
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||||
|
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||||
|
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||||
|
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||||
|
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||||
|
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||||
|
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||||
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||||
|
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||||
|
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||||
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
|
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||||
|
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||||
|
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
|
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
|
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
|
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||||
|
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||||
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||||
|
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||||
|
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
|
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||||
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
|
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||||
|
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
|
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||||
|
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||||
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||||
|
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||||
|
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||||
|
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||||
|
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||||
|
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||||
|
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||||
|
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
|
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
|
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||||
|
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||||
|
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||||
|
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||||
|
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||||
|
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||||
|
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||||
|
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
|
||||||
|
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
|
||||||
|
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||||
|
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||||
|
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||||
|
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||||
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
|
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||||
|
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
|
||||||
|
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
|
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||||
|
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||||
|
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||||
|
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||||
|
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||||
|
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||||
|
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||||
|
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||||
|
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||||
|
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||||
|
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||||
|
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||||
|
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||||
|
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||||
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
|
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||||
68
AttackSurface/src/gotestwaf/internal/config/config.go
Normal file
68
AttackSurface/src/gotestwaf/internal/config/config.go
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/waf_detector/detectors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
// Target settings
|
||||||
|
URL string `mapstructure:"url"`
|
||||||
|
GRPCPort uint16 `mapstructure:"grpcPort"`
|
||||||
|
GraphQLURL string `mapstructure:"graphqlURL"`
|
||||||
|
OpenAPIFile string `mapstructure:"openapiFile"`
|
||||||
|
|
||||||
|
// Test cases settings
|
||||||
|
TestCase string `mapstructure:"testCase"`
|
||||||
|
TestCasesPath string `mapstructure:"testCasesPath"`
|
||||||
|
TestSet string `mapstructure:"testSet"`
|
||||||
|
|
||||||
|
// HTTP client settings
|
||||||
|
HTTPClient string `mapstructure:"httpClient"`
|
||||||
|
TLSVerify bool `mapstructure:"tlsVerify"`
|
||||||
|
Proxy string `mapstructure:"proxy"`
|
||||||
|
AddHeader string `mapstructure:"addHeader"`
|
||||||
|
AddDebugHeader bool `mapstructure:"addDebugHeader"`
|
||||||
|
|
||||||
|
// GoHTTP client only settings
|
||||||
|
MaxIdleConns int `mapstructure:"maxIdleConns"`
|
||||||
|
MaxRedirects int `mapstructure:"maxRedirects"`
|
||||||
|
IdleConnTimeout int `mapstructure:"idleConnTimeout"`
|
||||||
|
FollowCookies bool `mapstructure:"followCookies"`
|
||||||
|
RenewSession bool `mapstructure:"renewSession"`
|
||||||
|
|
||||||
|
// Performance settings
|
||||||
|
Workers int `mapstructure:"workers"`
|
||||||
|
RandomDelay int `mapstructure:"randomDelay"`
|
||||||
|
SendDelay int `mapstructure:"sendDelay"`
|
||||||
|
|
||||||
|
// Analysis settings
|
||||||
|
SkipWAFBlockCheck bool `mapstructure:"skipWAFBlockCheck"`
|
||||||
|
SkipWAFIdentification bool `mapstructure:"skipWAFIdentification"`
|
||||||
|
BlockStatusCodes []int `mapstructure:"blockStatusCodes"`
|
||||||
|
PassStatusCodes []int `mapstructure:"passStatusCodes"`
|
||||||
|
BlockRegex string `mapstructure:"blockRegex"`
|
||||||
|
PassRegex string `mapstructure:"passRegex"`
|
||||||
|
NonBlockedAsPassed bool `mapstructure:"nonBlockedAsPassed"`
|
||||||
|
IgnoreUnresolved bool `mapstructure:"ignoreUnresolved"`
|
||||||
|
BlockConnReset bool `mapstructure:"blockConnReset"`
|
||||||
|
|
||||||
|
// Report settings
|
||||||
|
WAFName string `mapstructure:"wafName"`
|
||||||
|
IncludePayloads bool `mapstructure:"includePayloads"`
|
||||||
|
ReportPath string `mapstructure:"reportPath"`
|
||||||
|
ReportName string `mapstructure:"reportName"`
|
||||||
|
ReportFormat []string `mapstructure:"reportFormat"`
|
||||||
|
NoEmailReport bool `mapstructure:"noEmailReport"`
|
||||||
|
Email string `mapstructure:"email"`
|
||||||
|
HideArgsInReport bool `mapstructure:"hideArgsInReport"`
|
||||||
|
|
||||||
|
// config.yaml
|
||||||
|
HTTPHeaders map[string]string `mapstructure:"headers"`
|
||||||
|
|
||||||
|
// Other settings
|
||||||
|
LogLevel string `mapstructure:"logLevel"`
|
||||||
|
|
||||||
|
CheckBlockFunc detectors.Check
|
||||||
|
|
||||||
|
Args []string
|
||||||
|
}
|
||||||
112
AttackSurface/src/gotestwaf/internal/db/database.go
Normal file
112
AttackSurface/src/gotestwaf/internal/db/database.go
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
sync.Mutex
|
||||||
|
|
||||||
|
counters map[string]map[string]map[string]int
|
||||||
|
passedTests []*Info
|
||||||
|
blockedTests []*Info
|
||||||
|
failedTests []*Info
|
||||||
|
naTests []*Info
|
||||||
|
tests []*Case
|
||||||
|
|
||||||
|
scannedPaths map[string]map[string]interface{}
|
||||||
|
|
||||||
|
NumberOfTests uint
|
||||||
|
Hash string
|
||||||
|
|
||||||
|
IsGrpcAvailable bool
|
||||||
|
IsGraphQLAvailable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDB(tests []*Case) (*DB, error) {
|
||||||
|
db := &DB{
|
||||||
|
counters: make(map[string]map[string]map[string]int),
|
||||||
|
tests: tests,
|
||||||
|
}
|
||||||
|
|
||||||
|
var hashSums [][]byte
|
||||||
|
sha256hash := sha256.New()
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
if _, ok := db.counters[test.Set]; !ok {
|
||||||
|
db.counters[test.Set] = map[string]map[string]int{}
|
||||||
|
}
|
||||||
|
if _, ok := db.counters[test.Set][test.Name]; !ok {
|
||||||
|
db.counters[test.Set][test.Name] = map[string]int{}
|
||||||
|
}
|
||||||
|
|
||||||
|
db.NumberOfTests += uint(len(test.Payloads) * len(test.Encoders) * len(test.Placeholders))
|
||||||
|
|
||||||
|
hashSums = append(hashSums, test.Hash())
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(hashSums, func(i, j int) bool { return bytes.Compare(hashSums[i], hashSums[j]) < 0 })
|
||||||
|
|
||||||
|
sha256hash.Reset()
|
||||||
|
for i := range hashSums {
|
||||||
|
sha256hash.Write(hashSums[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Hash = hex.EncodeToString(sha256hash.Sum(nil)[:16])
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UpdatePassedTests(t *Info) {
|
||||||
|
db.Lock()
|
||||||
|
defer db.Unlock()
|
||||||
|
db.counters[t.Set][t.Case]["passed"]++
|
||||||
|
db.passedTests = append(db.passedTests, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UpdateNaTests(t *Info, ignoreUnresolved, nonBlockedAsPassed, isTruePositive bool) {
|
||||||
|
db.Lock()
|
||||||
|
defer db.Unlock()
|
||||||
|
if (ignoreUnresolved || nonBlockedAsPassed) && isTruePositive {
|
||||||
|
db.counters[t.Set][t.Case]["passed"]++
|
||||||
|
} else {
|
||||||
|
db.counters[t.Set][t.Case]["blocked"]++
|
||||||
|
}
|
||||||
|
db.naTests = append(db.naTests, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UpdateBlockedTests(t *Info) {
|
||||||
|
db.Lock()
|
||||||
|
defer db.Unlock()
|
||||||
|
db.counters[t.Set][t.Case]["blocked"]++
|
||||||
|
db.blockedTests = append(db.blockedTests, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UpdateFailedTests(t *Info) {
|
||||||
|
db.Lock()
|
||||||
|
defer db.Unlock()
|
||||||
|
db.counters[t.Set][t.Case]["failed"]++
|
||||||
|
db.failedTests = append(db.failedTests, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) AddToScannedPaths(method string, path string) {
|
||||||
|
db.Lock()
|
||||||
|
defer db.Unlock()
|
||||||
|
|
||||||
|
if db.scannedPaths == nil {
|
||||||
|
db.scannedPaths = make(map[string]map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := db.scannedPaths[path]; !ok {
|
||||||
|
db.scannedPaths[path] = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
db.scannedPaths[path][method] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetTestCases() []*Case {
|
||||||
|
return db.tests
|
||||||
|
}
|
||||||
117
AttackSurface/src/gotestwaf/internal/db/export.go
Normal file
117
AttackSurface/src/gotestwaf/internal/db/export.go
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/payload/encoder"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *DB) ExportPayloads(payloadsExportFile string) error {
|
||||||
|
csvFile, err := os.Create(payloadsExportFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer csvFile.Close()
|
||||||
|
|
||||||
|
csvWriter := csv.NewWriter(csvFile)
|
||||||
|
defer csvWriter.Flush()
|
||||||
|
|
||||||
|
if err := csvWriter.Write([]string{
|
||||||
|
"Payload",
|
||||||
|
"Check Status",
|
||||||
|
"Response Code",
|
||||||
|
"Placeholder",
|
||||||
|
"Encoder",
|
||||||
|
"Set",
|
||||||
|
"Case",
|
||||||
|
"Test Result",
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, blockedTest := range db.blockedTests {
|
||||||
|
p := blockedTest.Payload
|
||||||
|
e := blockedTest.Encoder
|
||||||
|
testResult := "passed"
|
||||||
|
|
||||||
|
ep, err := encoder.Apply(e, p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFalsePositiveTest(blockedTest.Set) {
|
||||||
|
testResult = "failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
err = csvWriter.Write([]string{
|
||||||
|
ep,
|
||||||
|
"blocked",
|
||||||
|
strconv.Itoa(blockedTest.ResponseStatusCode),
|
||||||
|
blockedTest.Placeholder,
|
||||||
|
blockedTest.Encoder,
|
||||||
|
blockedTest.Set,
|
||||||
|
blockedTest.Case,
|
||||||
|
testResult,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, passedTest := range db.passedTests {
|
||||||
|
p := passedTest.Payload
|
||||||
|
e := passedTest.Encoder
|
||||||
|
testResult := "failed"
|
||||||
|
|
||||||
|
ep, err := encoder.Apply(e, p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFalsePositiveTest(passedTest.Set) {
|
||||||
|
testResult = "passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
err = csvWriter.Write([]string{
|
||||||
|
ep,
|
||||||
|
"passed",
|
||||||
|
strconv.Itoa(passedTest.ResponseStatusCode),
|
||||||
|
passedTest.Placeholder,
|
||||||
|
passedTest.Encoder,
|
||||||
|
passedTest.Set,
|
||||||
|
passedTest.Case,
|
||||||
|
testResult,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, naTest := range db.naTests {
|
||||||
|
p := naTest.Payload
|
||||||
|
e := naTest.Encoder
|
||||||
|
|
||||||
|
ep, err := encoder.Apply(e, p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = csvWriter.Write([]string{
|
||||||
|
ep,
|
||||||
|
"unresolved",
|
||||||
|
strconv.Itoa(naTest.ResponseStatusCode),
|
||||||
|
naTest.Placeholder,
|
||||||
|
naTest.Encoder,
|
||||||
|
naTest.Set,
|
||||||
|
naTest.Case,
|
||||||
|
"unknown",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
56
AttackSurface/src/gotestwaf/internal/db/helpers.go
Normal file
56
AttackSurface/src/gotestwaf/internal/db/helpers.go
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Integer interface {
|
||||||
|
int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Float interface {
|
||||||
|
float32 | float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Number interface {
|
||||||
|
Integer | Float
|
||||||
|
}
|
||||||
|
|
||||||
|
func Round(n float64) float64 {
|
||||||
|
if math.IsNaN(n) {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
return math.Round(n*100) / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
func CalculatePercentage[A Number, B Number](first A, second B) float64 {
|
||||||
|
if second == 0 {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
result := float64(first) / float64(second) * 100
|
||||||
|
|
||||||
|
if math.IsNaN(result) {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
return Round(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFalsePositiveTest(setName string) bool {
|
||||||
|
return strings.Contains(setName, "false")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isApiTest(setName string) bool {
|
||||||
|
return strings.Contains(setName, "api")
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapToString(m map[any]any) string {
|
||||||
|
for k := range m {
|
||||||
|
return k.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
106
AttackSurface/src/gotestwaf/internal/db/load.go
Normal file
106
AttackSurface/src/gotestwaf/internal/db/load.go
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/config"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/payload/placeholder"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadTestCases(cfg *config.Config) (testCases []*Case, err error) {
|
||||||
|
var files []string
|
||||||
|
|
||||||
|
if cfg.TestCasesPath == "" {
|
||||||
|
return nil, errors.New("empty test cases path")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = filepath.Walk(cfg.TestCasesPath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
files = append(files, path)
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCaseFile := range files {
|
||||||
|
fileExt := filepath.Ext(testCaseFile)
|
||||||
|
if fileExt != ".yml" && fileExt != ".yaml" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore subdirectories, process as .../<testSetName>/<testCaseName>/<case>.yml
|
||||||
|
parts := strings.Split(testCaseFile, string(os.PathSeparator))
|
||||||
|
parts = parts[len(parts)-3:]
|
||||||
|
|
||||||
|
testSetName := parts[1]
|
||||||
|
testCaseName := strings.TrimSuffix(parts[2], fileExt)
|
||||||
|
|
||||||
|
if cfg.TestSet != "" && testSetName != cfg.TestSet {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.TestCase != "" && testCaseName != cfg.TestCase {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
yamlFile, err := os.ReadFile(testCaseFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var t yamlConfig
|
||||||
|
err = yaml.Unmarshal(yamlFile, &t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var placeholders []*Placeholder
|
||||||
|
for _, ph := range t.Placeholders {
|
||||||
|
switch typedPh := ph.(type) {
|
||||||
|
case string:
|
||||||
|
placeholders = append(placeholders, &Placeholder{Name: typedPh})
|
||||||
|
|
||||||
|
case map[any]any:
|
||||||
|
placeholderName := mapToString(typedPh)
|
||||||
|
placeholderConfig, confErr := placeholder.GetPlaceholderConfig(placeholderName, typedPh[placeholderName])
|
||||||
|
if confErr != nil {
|
||||||
|
return nil, errors.Wrap(confErr, "couldn't parse config")
|
||||||
|
}
|
||||||
|
|
||||||
|
placeholders = append(placeholders, &Placeholder{
|
||||||
|
Name: placeholderName,
|
||||||
|
Config: placeholderConfig,
|
||||||
|
})
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, errors.Errorf("couldn't parse config: unknown placeholder type, expected array of string or map[string]any, got %T", ph)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testCase := &Case{
|
||||||
|
Payloads: t.Payloads,
|
||||||
|
Encoders: t.Encoders,
|
||||||
|
Placeholders: placeholders,
|
||||||
|
Type: t.Type,
|
||||||
|
Set: testSetName,
|
||||||
|
Name: testCaseName,
|
||||||
|
IsTruePositive: true, // test case is true positive
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(testSetName, "false") {
|
||||||
|
testCase.IsTruePositive = false // test case is false positive
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases = append(testCases, testCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
if testCases == nil {
|
||||||
|
return nil, errors.New("no tests were selected")
|
||||||
|
}
|
||||||
|
|
||||||
|
return testCases, nil
|
||||||
|
}
|
||||||
86
AttackSurface/src/gotestwaf/internal/db/models.go
Normal file
86
AttackSurface/src/gotestwaf/internal/db/models.go
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/payload/placeholder"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/helpers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Info struct {
|
||||||
|
Payload string
|
||||||
|
Encoder string
|
||||||
|
Placeholder string
|
||||||
|
Set string
|
||||||
|
Case string
|
||||||
|
ResponseStatusCode int
|
||||||
|
AdditionalInfo []string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
|
||||||
|
type yamlConfig struct {
|
||||||
|
Payloads []string `yaml:"payload"`
|
||||||
|
Encoders []string `yaml:"encoder"`
|
||||||
|
Placeholders []any `yaml:"placeholder"` // array of string or map[string]any
|
||||||
|
Type string `default:"unknown" yaml:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Case struct {
|
||||||
|
Payloads []string
|
||||||
|
Encoders []string
|
||||||
|
Placeholders []*Placeholder
|
||||||
|
Type string
|
||||||
|
|
||||||
|
Set string
|
||||||
|
Name string
|
||||||
|
IsTruePositive bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ helpers.Hash = (*Case)(nil)
|
||||||
|
|
||||||
|
func (p *Case) Hash() []byte {
|
||||||
|
sha256sum := sha256.New()
|
||||||
|
|
||||||
|
for i := range p.Payloads {
|
||||||
|
sha256sum.Write([]byte(p.Payloads[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range p.Encoders {
|
||||||
|
sha256sum.Write([]byte(p.Encoders[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range p.Placeholders {
|
||||||
|
if p.Placeholders[i] != nil {
|
||||||
|
sha256sum.Write(p.Placeholders[i].Hash())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sha256sum.Write([]byte(p.Type))
|
||||||
|
sha256sum.Write([]byte(p.Set))
|
||||||
|
sha256sum.Write([]byte(p.Name))
|
||||||
|
|
||||||
|
if p.IsTruePositive {
|
||||||
|
sha256sum.Write([]byte{0x01})
|
||||||
|
} else {
|
||||||
|
sha256sum.Write([]byte{0x00})
|
||||||
|
}
|
||||||
|
|
||||||
|
return sha256sum.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Placeholder struct {
|
||||||
|
Name string
|
||||||
|
Config placeholder.PlaceholderConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ helpers.Hash = (*Placeholder)(nil)
|
||||||
|
|
||||||
|
func (p *Placeholder) Hash() []byte {
|
||||||
|
sha256sum := sha256.New()
|
||||||
|
sha256sum.Write([]byte(p.Name))
|
||||||
|
if p.Config != nil {
|
||||||
|
sha256sum.Write(p.Config.Hash())
|
||||||
|
}
|
||||||
|
return sha256sum.Sum(nil)
|
||||||
|
}
|
||||||
491
AttackSurface/src/gotestwaf/internal/db/statistics.go
Normal file
491
AttackSurface/src/gotestwaf/internal/db/statistics.go
Normal file
|
|
@ -0,0 +1,491 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Statistics struct {
|
||||||
|
IsGrpcAvailable bool
|
||||||
|
IsGraphQLAvailable bool
|
||||||
|
|
||||||
|
Paths ScannedPaths
|
||||||
|
|
||||||
|
TestCasesFingerprint string
|
||||||
|
|
||||||
|
TruePositiveTests TestsSummary
|
||||||
|
TrueNegativeTests TestsSummary
|
||||||
|
|
||||||
|
Score struct {
|
||||||
|
ApiSec Score
|
||||||
|
AppSec Score
|
||||||
|
Average float64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestsSummary struct {
|
||||||
|
SummaryTable []*SummaryTableRow
|
||||||
|
Blocked []*TestDetails
|
||||||
|
Bypasses []*TestDetails
|
||||||
|
Unresolved []*TestDetails
|
||||||
|
Failed []*FailedDetails
|
||||||
|
|
||||||
|
ReqStats RequestStats
|
||||||
|
ApiSecReqStats RequestStats
|
||||||
|
AppSecReqStats RequestStats
|
||||||
|
|
||||||
|
UnresolvedRequestsPercentage float64
|
||||||
|
ResolvedBlockedRequestsPercentage float64
|
||||||
|
ResolvedBypassedRequestsPercentage float64
|
||||||
|
FailedRequestsPercentage float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type SummaryTableRow struct {
|
||||||
|
TestSet string `json:"test_set" validate:"required,printascii,max=256"`
|
||||||
|
TestCase string `json:"test_case" validate:"required,printascii,max=256"`
|
||||||
|
Percentage float64 `json:"percentage" validate:"min=0,max=100"`
|
||||||
|
Sent int `json:"sent" validate:"min=0"`
|
||||||
|
Blocked int `json:"blocked" validate:"min=0"`
|
||||||
|
Bypassed int `json:"bypassed" validate:"min=0"`
|
||||||
|
Unresolved int `json:"unresolved" validate:"min=0"`
|
||||||
|
Failed int `json:"failed" validate:"min=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestDetails struct {
|
||||||
|
Payload string
|
||||||
|
TestCase string
|
||||||
|
TestSet string
|
||||||
|
Encoder string
|
||||||
|
Placeholder string
|
||||||
|
ResponseStatusCode int
|
||||||
|
AdditionalInfo []string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FailedDetails struct {
|
||||||
|
Payload string `json:"payload" validate:"required"`
|
||||||
|
TestCase string `json:"test_case" validate:"required,printascii"`
|
||||||
|
TestSet string `json:"test_set" validate:"required,printascii"`
|
||||||
|
Encoder string `json:"encoder" validate:"required,printascii"`
|
||||||
|
Placeholder string `json:"placeholder" validate:"required,printascii"`
|
||||||
|
Reason []string `json:"reason" validate:"omitempty,dive,required"`
|
||||||
|
Type string `json:"type" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestStats struct {
|
||||||
|
AllRequestsNumber int
|
||||||
|
BlockedRequestsNumber int
|
||||||
|
BypassedRequestsNumber int
|
||||||
|
UnresolvedRequestsNumber int
|
||||||
|
FailedRequestsNumber int
|
||||||
|
ResolvedRequestsNumber int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Score struct {
|
||||||
|
TruePositive float64
|
||||||
|
TrueNegative float64
|
||||||
|
Average float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Path struct {
|
||||||
|
Method string `json:"method" validate:"required,printascii,max=32"`
|
||||||
|
Path string `json:"path" validate:"required,printascii,max=1024"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScannedPaths []*Path
|
||||||
|
|
||||||
|
var _ sort.Interface = (ScannedPaths)(nil)
|
||||||
|
|
||||||
|
func (sp ScannedPaths) Len() int {
|
||||||
|
return len(sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sp ScannedPaths) Less(i, j int) bool {
|
||||||
|
if sp[i].Path > sp[j].Path {
|
||||||
|
return false
|
||||||
|
} else if sp[i].Path < sp[j].Path {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return sp[i].Method < sp[j].Method
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sp ScannedPaths) Swap(i, j int) {
|
||||||
|
sp[i], sp[j] = sp[j], sp[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sp ScannedPaths) Sort() {
|
||||||
|
sort.Sort(sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetStatistics(ignoreUnresolved, nonBlockedAsPassed bool) *Statistics {
|
||||||
|
db.Lock()
|
||||||
|
defer db.Unlock()
|
||||||
|
|
||||||
|
s := &Statistics{
|
||||||
|
IsGrpcAvailable: db.IsGrpcAvailable,
|
||||||
|
IsGraphQLAvailable: db.IsGraphQLAvailable,
|
||||||
|
TestCasesFingerprint: db.Hash,
|
||||||
|
}
|
||||||
|
|
||||||
|
unresolvedRequestsNumber := make(map[string]map[string]int)
|
||||||
|
|
||||||
|
for _, unresolvedTest := range db.naTests {
|
||||||
|
if unresolvedRequestsNumber[unresolvedTest.Set] == nil {
|
||||||
|
unresolvedRequestsNumber[unresolvedTest.Set] = make(map[string]int)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we want to count UNRESOLVED as BYPASSED, we shouldn't count UNRESOLVED at all
|
||||||
|
// set it to zero by default
|
||||||
|
if ignoreUnresolved || nonBlockedAsPassed {
|
||||||
|
unresolvedRequestsNumber[unresolvedTest.Set][unresolvedTest.Case] = 0
|
||||||
|
} else {
|
||||||
|
unresolvedRequestsNumber[unresolvedTest.Set][unresolvedTest.Case]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort all test sets by name
|
||||||
|
var sortedTestSets []string
|
||||||
|
for testSet := range db.counters {
|
||||||
|
sortedTestSets = append(sortedTestSets, testSet)
|
||||||
|
}
|
||||||
|
sort.Strings(sortedTestSets)
|
||||||
|
|
||||||
|
for _, testSet := range sortedTestSets {
|
||||||
|
// Sort all test cases by name
|
||||||
|
var sortedTestCases []string
|
||||||
|
for testCase := range db.counters[testSet] {
|
||||||
|
sortedTestCases = append(sortedTestCases, testCase)
|
||||||
|
}
|
||||||
|
sort.Strings(sortedTestCases)
|
||||||
|
|
||||||
|
isFalsePositive := isFalsePositiveTest(testSet)
|
||||||
|
|
||||||
|
for _, testCase := range sortedTestCases {
|
||||||
|
// Number of requests for all request types for the selected testCase
|
||||||
|
unresolvedRequests := unresolvedRequestsNumber[testSet][testCase]
|
||||||
|
passedRequests := db.counters[testSet][testCase]["passed"]
|
||||||
|
blockedRequests := db.counters[testSet][testCase]["blocked"]
|
||||||
|
failedRequests := db.counters[testSet][testCase]["failed"]
|
||||||
|
|
||||||
|
// passedRequests or blockedRequests already contains unresolvedRequests
|
||||||
|
totalRequests := passedRequests + blockedRequests + failedRequests
|
||||||
|
|
||||||
|
// If we don't want to count UNRESOLVED requests as BYPASSED, we need to subtract them
|
||||||
|
// from blocked requests (in other case we will count them as usual), and add this
|
||||||
|
// subtracted value to the overall requests
|
||||||
|
if !ignoreUnresolved || !nonBlockedAsPassed {
|
||||||
|
blockedRequests -= unresolvedRequests
|
||||||
|
}
|
||||||
|
|
||||||
|
totalResolvedRequests := passedRequests + blockedRequests
|
||||||
|
|
||||||
|
row := &SummaryTableRow{
|
||||||
|
TestSet: testSet,
|
||||||
|
TestCase: testCase,
|
||||||
|
Percentage: 0.0,
|
||||||
|
Sent: totalRequests,
|
||||||
|
Blocked: blockedRequests,
|
||||||
|
Bypassed: passedRequests,
|
||||||
|
Unresolved: unresolvedRequests,
|
||||||
|
Failed: failedRequests,
|
||||||
|
}
|
||||||
|
|
||||||
|
// If positive set - move to another table (remove from general cases)
|
||||||
|
if isFalsePositive {
|
||||||
|
// False positive - blocked by the WAF (bad behavior, blockedRequests)
|
||||||
|
s.TrueNegativeTests.ReqStats.BlockedRequestsNumber += blockedRequests
|
||||||
|
// True positive - bypassed (good behavior, passedRequests)
|
||||||
|
s.TrueNegativeTests.ReqStats.BypassedRequestsNumber += passedRequests
|
||||||
|
s.TrueNegativeTests.ReqStats.UnresolvedRequestsNumber += unresolvedRequests
|
||||||
|
s.TrueNegativeTests.ReqStats.FailedRequestsNumber += failedRequests
|
||||||
|
|
||||||
|
passedRequestsPercentage := CalculatePercentage(passedRequests, totalResolvedRequests)
|
||||||
|
row.Percentage = passedRequestsPercentage
|
||||||
|
|
||||||
|
s.TrueNegativeTests.SummaryTable = append(s.TrueNegativeTests.SummaryTable, row)
|
||||||
|
} else {
|
||||||
|
s.TruePositiveTests.ReqStats.BlockedRequestsNumber += blockedRequests
|
||||||
|
s.TruePositiveTests.ReqStats.BypassedRequestsNumber += passedRequests
|
||||||
|
s.TruePositiveTests.ReqStats.UnresolvedRequestsNumber += unresolvedRequests
|
||||||
|
s.TruePositiveTests.ReqStats.FailedRequestsNumber += failedRequests
|
||||||
|
|
||||||
|
blockedRequestsPercentage := CalculatePercentage(blockedRequests, totalResolvedRequests)
|
||||||
|
row.Percentage = blockedRequestsPercentage
|
||||||
|
|
||||||
|
s.TruePositiveTests.SummaryTable = append(s.TruePositiveTests.SummaryTable, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, blockedTest := range db.blockedTests {
|
||||||
|
sort.Strings(blockedTest.AdditionalInfo)
|
||||||
|
|
||||||
|
testDetails := &TestDetails{
|
||||||
|
Payload: blockedTest.Payload,
|
||||||
|
TestCase: blockedTest.Case,
|
||||||
|
TestSet: blockedTest.Set,
|
||||||
|
Encoder: blockedTest.Encoder,
|
||||||
|
Placeholder: blockedTest.Placeholder,
|
||||||
|
ResponseStatusCode: blockedTest.ResponseStatusCode,
|
||||||
|
AdditionalInfo: blockedTest.AdditionalInfo,
|
||||||
|
Type: blockedTest.Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFalsePositiveTest(blockedTest.Set) {
|
||||||
|
s.TrueNegativeTests.Blocked = append(s.TrueNegativeTests.Blocked, testDetails)
|
||||||
|
|
||||||
|
if isApiTest(blockedTest.Set) {
|
||||||
|
s.TrueNegativeTests.ApiSecReqStats.BlockedRequestsNumber += 1
|
||||||
|
} else {
|
||||||
|
s.TrueNegativeTests.AppSecReqStats.BlockedRequestsNumber += 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.TruePositiveTests.Blocked = append(s.TruePositiveTests.Blocked, testDetails)
|
||||||
|
|
||||||
|
if isApiTest(blockedTest.Set) {
|
||||||
|
s.TruePositiveTests.ApiSecReqStats.BlockedRequestsNumber += 1
|
||||||
|
} else {
|
||||||
|
s.TruePositiveTests.AppSecReqStats.BlockedRequestsNumber += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, passedTest := range db.passedTests {
|
||||||
|
sort.Strings(passedTest.AdditionalInfo)
|
||||||
|
|
||||||
|
testDetails := &TestDetails{
|
||||||
|
Payload: passedTest.Payload,
|
||||||
|
TestCase: passedTest.Case,
|
||||||
|
TestSet: passedTest.Set,
|
||||||
|
Encoder: passedTest.Encoder,
|
||||||
|
Placeholder: passedTest.Placeholder,
|
||||||
|
ResponseStatusCode: passedTest.ResponseStatusCode,
|
||||||
|
AdditionalInfo: passedTest.AdditionalInfo,
|
||||||
|
Type: passedTest.Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFalsePositiveTest(passedTest.Set) {
|
||||||
|
s.TrueNegativeTests.Bypasses = append(s.TrueNegativeTests.Bypasses, testDetails)
|
||||||
|
|
||||||
|
if isApiTest(passedTest.Set) {
|
||||||
|
s.TrueNegativeTests.ApiSecReqStats.BypassedRequestsNumber += 1
|
||||||
|
} else {
|
||||||
|
s.TrueNegativeTests.AppSecReqStats.BypassedRequestsNumber += 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.TruePositiveTests.Bypasses = append(s.TruePositiveTests.Bypasses, testDetails)
|
||||||
|
|
||||||
|
if isApiTest(passedTest.Set) {
|
||||||
|
s.TruePositiveTests.ApiSecReqStats.BypassedRequestsNumber += 1
|
||||||
|
} else {
|
||||||
|
s.TruePositiveTests.AppSecReqStats.BypassedRequestsNumber += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, unresolvedTest := range db.naTests {
|
||||||
|
sort.Strings(unresolvedTest.AdditionalInfo)
|
||||||
|
|
||||||
|
testDetails := &TestDetails{
|
||||||
|
Payload: unresolvedTest.Payload,
|
||||||
|
TestCase: unresolvedTest.Case,
|
||||||
|
TestSet: unresolvedTest.Set,
|
||||||
|
Encoder: unresolvedTest.Encoder,
|
||||||
|
Placeholder: unresolvedTest.Placeholder,
|
||||||
|
ResponseStatusCode: unresolvedTest.ResponseStatusCode,
|
||||||
|
AdditionalInfo: unresolvedTest.AdditionalInfo,
|
||||||
|
Type: unresolvedTest.Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ignoreUnresolved || nonBlockedAsPassed {
|
||||||
|
if isFalsePositiveTest(unresolvedTest.Set) {
|
||||||
|
s.TrueNegativeTests.Blocked = append(s.TrueNegativeTests.Blocked, testDetails)
|
||||||
|
|
||||||
|
if isApiTest(unresolvedTest.Set) {
|
||||||
|
s.TrueNegativeTests.ApiSecReqStats.BlockedRequestsNumber += 1
|
||||||
|
} else {
|
||||||
|
s.TrueNegativeTests.AppSecReqStats.BlockedRequestsNumber += 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.TruePositiveTests.Bypasses = append(s.TruePositiveTests.Bypasses, testDetails)
|
||||||
|
|
||||||
|
if isApiTest(unresolvedTest.Set) {
|
||||||
|
s.TruePositiveTests.ApiSecReqStats.BypassedRequestsNumber += 1
|
||||||
|
} else {
|
||||||
|
s.TruePositiveTests.AppSecReqStats.BypassedRequestsNumber += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if isFalsePositiveTest(unresolvedTest.Set) {
|
||||||
|
s.TrueNegativeTests.Unresolved = append(s.TrueNegativeTests.Unresolved, testDetails)
|
||||||
|
|
||||||
|
if isApiTest(unresolvedTest.Set) {
|
||||||
|
s.TrueNegativeTests.ApiSecReqStats.UnresolvedRequestsNumber += 1
|
||||||
|
} else {
|
||||||
|
s.TrueNegativeTests.AppSecReqStats.UnresolvedRequestsNumber += 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.TruePositiveTests.Unresolved = append(s.TruePositiveTests.Unresolved, testDetails)
|
||||||
|
|
||||||
|
if isApiTest(unresolvedTest.Set) {
|
||||||
|
s.TruePositiveTests.ApiSecReqStats.UnresolvedRequestsNumber += 1
|
||||||
|
} else {
|
||||||
|
s.TruePositiveTests.AppSecReqStats.UnresolvedRequestsNumber += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, failedTest := range db.failedTests {
|
||||||
|
testDetails := &FailedDetails{
|
||||||
|
Payload: failedTest.Payload,
|
||||||
|
TestCase: failedTest.Case,
|
||||||
|
TestSet: failedTest.Set,
|
||||||
|
Encoder: failedTest.Encoder,
|
||||||
|
Placeholder: failedTest.Placeholder,
|
||||||
|
Reason: failedTest.AdditionalInfo,
|
||||||
|
Type: failedTest.Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFalsePositiveTest(failedTest.Set) {
|
||||||
|
s.TrueNegativeTests.Failed = append(s.TrueNegativeTests.Failed, testDetails)
|
||||||
|
|
||||||
|
if isApiTest(failedTest.Set) {
|
||||||
|
s.TrueNegativeTests.ApiSecReqStats.FailedRequestsNumber += 1
|
||||||
|
} else {
|
||||||
|
s.TrueNegativeTests.AppSecReqStats.FailedRequestsNumber += 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.TruePositiveTests.Failed = append(s.TruePositiveTests.Failed, testDetails)
|
||||||
|
|
||||||
|
if isApiTest(failedTest.Set) {
|
||||||
|
s.TruePositiveTests.ApiSecReqStats.FailedRequestsNumber += 1
|
||||||
|
} else {
|
||||||
|
s.TruePositiveTests.AppSecReqStats.FailedRequestsNumber += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if db.scannedPaths != nil {
|
||||||
|
var paths ScannedPaths
|
||||||
|
for path, methods := range db.scannedPaths {
|
||||||
|
for method := range methods {
|
||||||
|
paths = append(paths, &Path{
|
||||||
|
Method: method,
|
||||||
|
Path: path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paths.Sort()
|
||||||
|
|
||||||
|
s.Paths = paths
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateTestsSummaryStat(&s.TruePositiveTests)
|
||||||
|
calculateTestsSummaryStat(&s.TrueNegativeTests)
|
||||||
|
|
||||||
|
calculateScorePercentage(
|
||||||
|
&s.Score.ApiSec,
|
||||||
|
s.TruePositiveTests.ApiSecReqStats.BlockedRequestsNumber,
|
||||||
|
s.TruePositiveTests.ApiSecReqStats.ResolvedRequestsNumber,
|
||||||
|
s.TrueNegativeTests.ApiSecReqStats.BypassedRequestsNumber,
|
||||||
|
s.TrueNegativeTests.ApiSecReqStats.ResolvedRequestsNumber,
|
||||||
|
)
|
||||||
|
calculateScorePercentage(
|
||||||
|
&s.Score.AppSec,
|
||||||
|
s.TruePositiveTests.AppSecReqStats.BlockedRequestsNumber,
|
||||||
|
s.TruePositiveTests.AppSecReqStats.ResolvedRequestsNumber,
|
||||||
|
s.TrueNegativeTests.AppSecReqStats.BypassedRequestsNumber,
|
||||||
|
s.TrueNegativeTests.AppSecReqStats.ResolvedRequestsNumber,
|
||||||
|
)
|
||||||
|
|
||||||
|
var divider int
|
||||||
|
var sum float64
|
||||||
|
|
||||||
|
if s.Score.ApiSec.Average != -1.0 {
|
||||||
|
divider++
|
||||||
|
sum += s.Score.ApiSec.Average
|
||||||
|
}
|
||||||
|
if s.Score.AppSec.Average != -1.0 {
|
||||||
|
divider++
|
||||||
|
sum += s.Score.AppSec.Average
|
||||||
|
}
|
||||||
|
|
||||||
|
if divider != 0 {
|
||||||
|
s.Score.Average = Round(sum / float64(divider))
|
||||||
|
} else {
|
||||||
|
s.Score.Average = -1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateTestsSummaryStat(s *TestsSummary) {
|
||||||
|
// All requests stat
|
||||||
|
s.ReqStats.AllRequestsNumber = s.ReqStats.BlockedRequestsNumber +
|
||||||
|
s.ReqStats.BypassedRequestsNumber +
|
||||||
|
s.ReqStats.UnresolvedRequestsNumber +
|
||||||
|
s.ReqStats.FailedRequestsNumber
|
||||||
|
|
||||||
|
s.ReqStats.ResolvedRequestsNumber = s.ReqStats.BlockedRequestsNumber +
|
||||||
|
s.ReqStats.BypassedRequestsNumber
|
||||||
|
|
||||||
|
// ApiSec requests stat
|
||||||
|
s.ApiSecReqStats.AllRequestsNumber = s.ApiSecReqStats.BlockedRequestsNumber +
|
||||||
|
s.ApiSecReqStats.BypassedRequestsNumber +
|
||||||
|
s.ApiSecReqStats.UnresolvedRequestsNumber +
|
||||||
|
s.ApiSecReqStats.FailedRequestsNumber
|
||||||
|
|
||||||
|
s.ApiSecReqStats.ResolvedRequestsNumber = s.ApiSecReqStats.BlockedRequestsNumber +
|
||||||
|
s.ApiSecReqStats.BypassedRequestsNumber
|
||||||
|
|
||||||
|
// AppSec requests stat
|
||||||
|
s.AppSecReqStats.AllRequestsNumber = s.AppSecReqStats.BlockedRequestsNumber +
|
||||||
|
s.AppSecReqStats.BypassedRequestsNumber +
|
||||||
|
s.AppSecReqStats.UnresolvedRequestsNumber +
|
||||||
|
s.AppSecReqStats.FailedRequestsNumber
|
||||||
|
|
||||||
|
s.AppSecReqStats.ResolvedRequestsNumber = s.AppSecReqStats.BlockedRequestsNumber +
|
||||||
|
s.AppSecReqStats.BypassedRequestsNumber
|
||||||
|
|
||||||
|
s.UnresolvedRequestsPercentage = CalculatePercentage(s.ReqStats.UnresolvedRequestsNumber, s.ReqStats.AllRequestsNumber)
|
||||||
|
s.ResolvedBlockedRequestsPercentage = CalculatePercentage(s.ReqStats.BlockedRequestsNumber, s.ReqStats.ResolvedRequestsNumber)
|
||||||
|
s.ResolvedBypassedRequestsPercentage = CalculatePercentage(s.ReqStats.BypassedRequestsNumber, s.ReqStats.ResolvedRequestsNumber)
|
||||||
|
s.FailedRequestsPercentage = CalculatePercentage(s.ReqStats.FailedRequestsNumber, s.ReqStats.AllRequestsNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateScorePercentage(s *Score, truePosBlockedNum, truePosNum, trueNegBypassNum, trueNegNum int) {
|
||||||
|
var (
|
||||||
|
divider int
|
||||||
|
sum float64
|
||||||
|
)
|
||||||
|
|
||||||
|
s.TruePositive = CalculatePercentage(truePosBlockedNum, truePosNum)
|
||||||
|
s.TrueNegative = CalculatePercentage(trueNegBypassNum, trueNegNum)
|
||||||
|
|
||||||
|
if truePosNum != 0 {
|
||||||
|
divider++
|
||||||
|
sum += s.TruePositive
|
||||||
|
} else {
|
||||||
|
s.TruePositive = -1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
if trueNegNum != 0 {
|
||||||
|
divider++
|
||||||
|
sum += s.TrueNegative
|
||||||
|
} else {
|
||||||
|
s.TrueNegative = -1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
if divider != 0 {
|
||||||
|
// If all malicious request were passed then grade is 0.
|
||||||
|
if truePosBlockedNum == 0 {
|
||||||
|
s.Average = 0.0
|
||||||
|
} else {
|
||||||
|
s.Average = Round(sum / float64(divider))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.Average = -1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
652
AttackSurface/src/gotestwaf/internal/db/statistics_test.go
Normal file
652
AttackSurface/src/gotestwaf/internal/db/statistics_test.go
Normal file
|
|
@ -0,0 +1,652 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/leanovate/gopter"
|
||||||
|
"github.com/leanovate/gopter/gen"
|
||||||
|
"github.com/leanovate/gopter/prop"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStatistics(t *testing.T) {
|
||||||
|
bools := []bool{false, true}
|
||||||
|
|
||||||
|
parameters := gopter.DefaultTestParameters()
|
||||||
|
parameters.MinSuccessfulTests = 1000
|
||||||
|
|
||||||
|
properties := gopter.NewProperties(parameters)
|
||||||
|
|
||||||
|
// testPropertyNotPanics
|
||||||
|
for _, b1 := range bools {
|
||||||
|
for _, b2 := range bools {
|
||||||
|
properties.Property(
|
||||||
|
fmt.Sprintf("testPropertyNotPanics(%v, %v)-NewDBAllPassedGenerator", b1, b2),
|
||||||
|
prop.ForAllNoShrink(
|
||||||
|
testPropertyNotPanics,
|
||||||
|
NewDBAllPassedGenerator(),
|
||||||
|
BoolGenerator(b1),
|
||||||
|
BoolGenerator(b2)))
|
||||||
|
properties.Property(
|
||||||
|
fmt.Sprintf("testPropertyNotPanics(%v, %v)-NewDBAllBlockedGenerator", b1, b2),
|
||||||
|
prop.ForAllNoShrink(
|
||||||
|
testPropertyNotPanics,
|
||||||
|
NewDBAllBlockedGenerator(),
|
||||||
|
BoolGenerator(b1),
|
||||||
|
BoolGenerator(b2)))
|
||||||
|
properties.Property(
|
||||||
|
fmt.Sprintf("testPropertyNotPanics(%v, %v)-NewDBAllFailedGenerator", b1, b2),
|
||||||
|
prop.ForAllNoShrink(
|
||||||
|
testPropertyNotPanics,
|
||||||
|
NewDBAllFailedGenerator(),
|
||||||
|
BoolGenerator(b1),
|
||||||
|
BoolGenerator(b2)))
|
||||||
|
properties.Property(
|
||||||
|
fmt.Sprintf("testPropertyNotPanics(%v, %v)-NewDBAllUnresolvedGenerator(%[1]v, %[2]v)", b1, b2),
|
||||||
|
prop.ForAllNoShrink(
|
||||||
|
testPropertyNotPanics,
|
||||||
|
NewDBAllUnresolvedGenerator(b1, b2),
|
||||||
|
BoolGenerator(b1),
|
||||||
|
BoolGenerator(b2)))
|
||||||
|
properties.Property(
|
||||||
|
fmt.Sprintf("testPropertyNotPanics(%v, %v)-NewDBGenerator(%[1]v, %[2]v)", b1, b2),
|
||||||
|
prop.ForAllNoShrink(
|
||||||
|
testPropertyNotPanics,
|
||||||
|
NewDBGenerator(b1, b2),
|
||||||
|
BoolGenerator(b1),
|
||||||
|
BoolGenerator(b2)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// testPropertyOnlyPositiveNumberValues
|
||||||
|
for _, b1 := range bools {
|
||||||
|
for _, b2 := range bools {
|
||||||
|
properties.Property(
|
||||||
|
fmt.Sprintf("testPropertyOnlyPositiveNumberValues(%v, %v)-NewDBAllPassedGenerator", b1, b2),
|
||||||
|
prop.ForAllNoShrink(
|
||||||
|
testPropertyOnlyPositiveNumberValues,
|
||||||
|
NewDBAllPassedGenerator(),
|
||||||
|
BoolGenerator(b1),
|
||||||
|
BoolGenerator(b2)))
|
||||||
|
properties.Property(
|
||||||
|
fmt.Sprintf("testPropertyOnlyPositiveNumberValues(%v, %v)-NewDBAllBlockedGenerator", b1, b2),
|
||||||
|
prop.ForAllNoShrink(
|
||||||
|
testPropertyOnlyPositiveNumberValues,
|
||||||
|
NewDBAllBlockedGenerator(),
|
||||||
|
BoolGenerator(b1),
|
||||||
|
BoolGenerator(b2)))
|
||||||
|
properties.Property(
|
||||||
|
fmt.Sprintf("testPropertyOnlyPositiveNumberValues(%v, %v)-NewDBAllFailedGenerator", b1, b2),
|
||||||
|
prop.ForAllNoShrink(
|
||||||
|
testPropertyOnlyPositiveNumberValues,
|
||||||
|
NewDBAllFailedGenerator(),
|
||||||
|
BoolGenerator(b1),
|
||||||
|
BoolGenerator(b2)))
|
||||||
|
properties.Property(
|
||||||
|
fmt.Sprintf("testPropertyOnlyPositiveNumberValues(%v, %v)-NewDBAllUnresolvedGenerator(%[1]v, %[2]v)", b1, b2),
|
||||||
|
prop.ForAllNoShrink(
|
||||||
|
testPropertyOnlyPositiveNumberValues,
|
||||||
|
NewDBAllUnresolvedGenerator(b1, b2),
|
||||||
|
BoolGenerator(b1),
|
||||||
|
BoolGenerator(b2)))
|
||||||
|
properties.Property(
|
||||||
|
fmt.Sprintf("testPropertyOnlyPositiveNumberValues(%v, %v)-NewDBGenerator(%[1]v, %[2]v)", b1, b2),
|
||||||
|
prop.ForAllNoShrink(
|
||||||
|
testPropertyOnlyPositiveNumberValues,
|
||||||
|
NewDBGenerator(b1, b2),
|
||||||
|
BoolGenerator(b1),
|
||||||
|
BoolGenerator(b2)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// testPropertyCorrectStatValues
|
||||||
|
for _, b1 := range bools {
|
||||||
|
for _, b2 := range bools {
|
||||||
|
properties.Property(
|
||||||
|
fmt.Sprintf("testPropertyCorrectStatValues(%v, %v)-NewDBAllPassedGenerator", b1, b2),
|
||||||
|
prop.ForAllNoShrink(
|
||||||
|
testPropertyCorrectStatValues,
|
||||||
|
NewDBAllPassedGenerator(),
|
||||||
|
BoolGenerator(b1),
|
||||||
|
BoolGenerator(b2)))
|
||||||
|
properties.Property(
|
||||||
|
fmt.Sprintf("testPropertyCorrectStatValues(%v, %v)-NewDBAllBlockedGenerator", b1, b2),
|
||||||
|
prop.ForAllNoShrink(
|
||||||
|
testPropertyCorrectStatValues,
|
||||||
|
NewDBAllBlockedGenerator(),
|
||||||
|
BoolGenerator(b1),
|
||||||
|
BoolGenerator(b2)))
|
||||||
|
properties.Property(
|
||||||
|
fmt.Sprintf("testPropertyCorrectStatValues(%v, %v)-NewDBAllFailedGenerator", b1, b2),
|
||||||
|
prop.ForAllNoShrink(
|
||||||
|
testPropertyCorrectStatValues,
|
||||||
|
NewDBAllFailedGenerator(),
|
||||||
|
BoolGenerator(b1),
|
||||||
|
BoolGenerator(b2)))
|
||||||
|
properties.Property(
|
||||||
|
fmt.Sprintf("testPropertyCorrectStatValues(%v, %v)-NewDBAllUnresolvedGenerator(%[1]v, %[2]v)", b1, b2),
|
||||||
|
prop.ForAllNoShrink(
|
||||||
|
testPropertyCorrectStatValues,
|
||||||
|
NewDBAllUnresolvedGenerator(b1, b2),
|
||||||
|
BoolGenerator(b1),
|
||||||
|
BoolGenerator(b2)))
|
||||||
|
properties.Property(
|
||||||
|
fmt.Sprintf("testPropertyCorrectStatValues(%v, %v)-NewDBGenerator(%[1]v, %[2]v)", b1, b2),
|
||||||
|
prop.ForAllNoShrink(
|
||||||
|
testPropertyCorrectStatValues,
|
||||||
|
NewDBGenerator(b1, b2),
|
||||||
|
BoolGenerator(b1),
|
||||||
|
BoolGenerator(b2)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
properties.TestingRun(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPropertyNotPanics(db *DB, ignoreUnresolved, nonBlockedAsPassed bool) bool {
|
||||||
|
var err interface{}
|
||||||
|
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
err = recover()
|
||||||
|
}()
|
||||||
|
|
||||||
|
_ = db.GetStatistics(ignoreUnresolved, nonBlockedAsPassed)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPropertyOnlyPositiveNumberValues(db *DB, ignoreUnresolved, nonBlockedAsPassed bool) bool {
|
||||||
|
stat := db.GetStatistics(ignoreUnresolved, nonBlockedAsPassed)
|
||||||
|
|
||||||
|
if stat.TruePositiveTests.ReqStats.AllRequestsNumber < 0 ||
|
||||||
|
stat.TruePositiveTests.ReqStats.BlockedRequestsNumber < 0 ||
|
||||||
|
stat.TruePositiveTests.ReqStats.BypassedRequestsNumber < 0 ||
|
||||||
|
stat.TruePositiveTests.ReqStats.UnresolvedRequestsNumber < 0 ||
|
||||||
|
stat.TruePositiveTests.ReqStats.FailedRequestsNumber < 0 ||
|
||||||
|
stat.TruePositiveTests.ReqStats.ResolvedRequestsNumber < 0 ||
|
||||||
|
stat.TruePositiveTests.UnresolvedRequestsPercentage < 0 ||
|
||||||
|
stat.TruePositiveTests.ResolvedBlockedRequestsPercentage < 0 ||
|
||||||
|
stat.TruePositiveTests.ResolvedBypassedRequestsPercentage < 0 ||
|
||||||
|
stat.TruePositiveTests.FailedRequestsPercentage < 0 ||
|
||||||
|
stat.TrueNegativeTests.ReqStats.AllRequestsNumber < 0 ||
|
||||||
|
stat.TrueNegativeTests.ReqStats.BlockedRequestsNumber < 0 ||
|
||||||
|
stat.TrueNegativeTests.ReqStats.BypassedRequestsNumber < 0 ||
|
||||||
|
stat.TrueNegativeTests.ReqStats.UnresolvedRequestsNumber < 0 ||
|
||||||
|
stat.TrueNegativeTests.ReqStats.FailedRequestsNumber < 0 ||
|
||||||
|
stat.TrueNegativeTests.ReqStats.ResolvedRequestsNumber < 0 ||
|
||||||
|
stat.TrueNegativeTests.UnresolvedRequestsPercentage < 0 ||
|
||||||
|
stat.TrueNegativeTests.ResolvedBlockedRequestsPercentage < 0 ||
|
||||||
|
stat.TrueNegativeTests.ResolvedBypassedRequestsPercentage < 0 ||
|
||||||
|
stat.TrueNegativeTests.FailedRequestsPercentage < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryTablesRows := append(stat.TruePositiveTests.SummaryTable, stat.TrueNegativeTests.SummaryTable...)
|
||||||
|
for _, row := range summaryTablesRows {
|
||||||
|
if row.Percentage < 0 ||
|
||||||
|
row.Sent < 0 ||
|
||||||
|
row.Blocked < 0 ||
|
||||||
|
row.Bypassed < 0 ||
|
||||||
|
row.Unresolved < 0 ||
|
||||||
|
row.Failed < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPropertyCorrectStatValues(db *DB, ignoreUnresolved, nonBlockedAsPassed bool) bool {
|
||||||
|
stat := db.GetStatistics(ignoreUnresolved, nonBlockedAsPassed)
|
||||||
|
|
||||||
|
counters := make(map[string]map[string]int)
|
||||||
|
counters["true-positive"] = make(map[string]int)
|
||||||
|
counters["true-negative"] = make(map[string]int)
|
||||||
|
|
||||||
|
for _, row := range stat.TruePositiveTests.SummaryTable {
|
||||||
|
counters["true-positive"]["sent"] += row.Sent
|
||||||
|
counters["true-positive"]["blocked"] += row.Blocked
|
||||||
|
counters["true-positive"]["bypassed"] += row.Bypassed
|
||||||
|
counters["true-positive"]["unresolved"] += row.Unresolved
|
||||||
|
counters["true-positive"]["failed"] += row.Failed
|
||||||
|
}
|
||||||
|
|
||||||
|
counters["true-positive"]["all"] = counters["true-positive"]["blocked"] +
|
||||||
|
counters["true-positive"]["bypassed"] +
|
||||||
|
counters["true-positive"]["unresolved"] +
|
||||||
|
counters["true-positive"]["failed"]
|
||||||
|
|
||||||
|
counters["true-positive"]["resolved"] = counters["true-positive"]["blocked"] +
|
||||||
|
counters["true-positive"]["bypassed"]
|
||||||
|
|
||||||
|
if counters["true-positive"]["all"] != stat.TruePositiveTests.ReqStats.AllRequestsNumber ||
|
||||||
|
counters["true-positive"]["blocked"] != stat.TruePositiveTests.ReqStats.BlockedRequestsNumber ||
|
||||||
|
counters["true-positive"]["bypassed"] != stat.TruePositiveTests.ReqStats.BypassedRequestsNumber ||
|
||||||
|
counters["true-positive"]["unresolved"] != stat.TruePositiveTests.ReqStats.UnresolvedRequestsNumber ||
|
||||||
|
counters["true-positive"]["failed"] != stat.TruePositiveTests.ReqStats.FailedRequestsNumber ||
|
||||||
|
counters["true-positive"]["resolved"] != stat.TruePositiveTests.ReqStats.ResolvedRequestsNumber {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range stat.TrueNegativeTests.SummaryTable {
|
||||||
|
counters["true-negative"]["sent"] += row.Sent
|
||||||
|
counters["true-negative"]["blocked"] += row.Blocked
|
||||||
|
counters["true-negative"]["bypassed"] += row.Bypassed
|
||||||
|
counters["true-negative"]["unresolved"] += row.Unresolved
|
||||||
|
counters["true-negative"]["failed"] += row.Failed
|
||||||
|
}
|
||||||
|
|
||||||
|
counters["true-negative"]["all"] = counters["true-negative"]["blocked"] +
|
||||||
|
counters["true-negative"]["bypassed"] +
|
||||||
|
counters["true-negative"]["unresolved"] +
|
||||||
|
counters["true-negative"]["failed"]
|
||||||
|
|
||||||
|
counters["true-negative"]["resolved"] = counters["true-negative"]["blocked"] +
|
||||||
|
counters["true-negative"]["bypassed"]
|
||||||
|
|
||||||
|
if counters["true-negative"]["all"] != stat.TrueNegativeTests.ReqStats.AllRequestsNumber ||
|
||||||
|
counters["true-negative"]["blocked"] != stat.TrueNegativeTests.ReqStats.BlockedRequestsNumber ||
|
||||||
|
counters["true-negative"]["bypassed"] != stat.TrueNegativeTests.ReqStats.BypassedRequestsNumber ||
|
||||||
|
counters["true-negative"]["unresolved"] != stat.TrueNegativeTests.ReqStats.UnresolvedRequestsNumber ||
|
||||||
|
counters["true-negative"]["failed"] != stat.TrueNegativeTests.ReqStats.FailedRequestsNumber ||
|
||||||
|
counters["true-negative"]["resolved"] != stat.TrueNegativeTests.ReqStats.ResolvedRequestsNumber {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDBAllPassedGenerator() gopter.Gen {
|
||||||
|
return gopter.DeriveGen(
|
||||||
|
func(passedTests []*Info) *DB {
|
||||||
|
db := &DB{
|
||||||
|
counters: make(map[string]map[string]map[string]int),
|
||||||
|
passedTests: passedTests,
|
||||||
|
NumberOfTests: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range passedTests {
|
||||||
|
if db.counters[t.Set] == nil {
|
||||||
|
db.counters[t.Set] = make(map[string]map[string]int)
|
||||||
|
}
|
||||||
|
if db.counters[t.Set][t.Case] == nil {
|
||||||
|
db.counters[t.Set][t.Case] = make(map[string]int)
|
||||||
|
}
|
||||||
|
db.counters[t.Set][t.Case]["passed"] += 1
|
||||||
|
db.NumberOfTests += 1
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
},
|
||||||
|
func(db *DB) []*Info {
|
||||||
|
return db.passedTests
|
||||||
|
},
|
||||||
|
GenInfoSlice(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDBAllBlockedGenerator() gopter.Gen {
|
||||||
|
return gopter.DeriveGen(
|
||||||
|
func(blockedTests []*Info) *DB {
|
||||||
|
db := &DB{
|
||||||
|
counters: make(map[string]map[string]map[string]int),
|
||||||
|
blockedTests: blockedTests,
|
||||||
|
NumberOfTests: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range blockedTests {
|
||||||
|
if db.counters[t.Set] == nil {
|
||||||
|
db.counters[t.Set] = make(map[string]map[string]int)
|
||||||
|
}
|
||||||
|
if db.counters[t.Set][t.Case] == nil {
|
||||||
|
db.counters[t.Set][t.Case] = make(map[string]int)
|
||||||
|
}
|
||||||
|
db.counters[t.Set][t.Case]["blocked"] += 1
|
||||||
|
db.NumberOfTests += 1
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
},
|
||||||
|
func(db *DB) []*Info {
|
||||||
|
return db.blockedTests
|
||||||
|
},
|
||||||
|
GenInfoSlice(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDBAllUnresolvedGenerator(ignoreUnresolved, nonBlockedAsPassed bool) gopter.Gen {
|
||||||
|
return gopter.DeriveGen(
|
||||||
|
func(unresolvedTests []*Info) *DB {
|
||||||
|
db := &DB{
|
||||||
|
counters: make(map[string]map[string]map[string]int),
|
||||||
|
naTests: unresolvedTests,
|
||||||
|
NumberOfTests: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range unresolvedTests {
|
||||||
|
if db.counters[t.Set] == nil {
|
||||||
|
db.counters[t.Set] = make(map[string]map[string]int)
|
||||||
|
}
|
||||||
|
if db.counters[t.Set][t.Case] == nil {
|
||||||
|
db.counters[t.Set][t.Case] = make(map[string]int)
|
||||||
|
}
|
||||||
|
if (ignoreUnresolved || nonBlockedAsPassed) && !isFalsePositiveTest(t.Set) {
|
||||||
|
db.counters[t.Set][t.Case]["passed"]++
|
||||||
|
} else {
|
||||||
|
db.counters[t.Set][t.Case]["blocked"]++
|
||||||
|
}
|
||||||
|
db.NumberOfTests += 1
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
},
|
||||||
|
func(db *DB) []*Info {
|
||||||
|
return db.naTests
|
||||||
|
},
|
||||||
|
GenInfoSlice(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDBAllFailedGenerator() gopter.Gen {
|
||||||
|
return gopter.DeriveGen(
|
||||||
|
func(failedTests []*Info) *DB {
|
||||||
|
db := &DB{
|
||||||
|
counters: make(map[string]map[string]map[string]int),
|
||||||
|
failedTests: failedTests,
|
||||||
|
NumberOfTests: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range failedTests {
|
||||||
|
if db.counters[t.Set] == nil {
|
||||||
|
db.counters[t.Set] = make(map[string]map[string]int)
|
||||||
|
}
|
||||||
|
if db.counters[t.Set][t.Case] == nil {
|
||||||
|
db.counters[t.Set][t.Case] = make(map[string]int)
|
||||||
|
}
|
||||||
|
db.counters[t.Set][t.Case]["failed"] += 1
|
||||||
|
db.NumberOfTests += 1
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
},
|
||||||
|
func(db *DB) []*Info {
|
||||||
|
return db.failedTests
|
||||||
|
},
|
||||||
|
GenInfoSlice(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDBGenerator(ignoreUnresolved, nonBlockedAsPassed bool) gopter.Gen {
|
||||||
|
return gopter.DeriveGen(
|
||||||
|
func(passedTests, blockedTests, failedTests, unresolvedTests []*Info) *DB {
|
||||||
|
db := &DB{
|
||||||
|
counters: make(map[string]map[string]map[string]int),
|
||||||
|
passedTests: passedTests,
|
||||||
|
blockedTests: blockedTests,
|
||||||
|
failedTests: failedTests,
|
||||||
|
naTests: unresolvedTests,
|
||||||
|
NumberOfTests: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range passedTests {
|
||||||
|
if db.counters[t.Set] == nil {
|
||||||
|
db.counters[t.Set] = make(map[string]map[string]int)
|
||||||
|
}
|
||||||
|
if db.counters[t.Set][t.Case] == nil {
|
||||||
|
db.counters[t.Set][t.Case] = make(map[string]int)
|
||||||
|
}
|
||||||
|
db.counters[t.Set][t.Case]["passed"] += 1
|
||||||
|
db.NumberOfTests += 1
|
||||||
|
}
|
||||||
|
for _, t := range blockedTests {
|
||||||
|
if db.counters[t.Set] == nil {
|
||||||
|
db.counters[t.Set] = make(map[string]map[string]int)
|
||||||
|
}
|
||||||
|
if db.counters[t.Set][t.Case] == nil {
|
||||||
|
db.counters[t.Set][t.Case] = make(map[string]int)
|
||||||
|
}
|
||||||
|
db.counters[t.Set][t.Case]["blocked"] += 1
|
||||||
|
db.NumberOfTests += 1
|
||||||
|
}
|
||||||
|
for _, t := range failedTests {
|
||||||
|
if db.counters[t.Set] == nil {
|
||||||
|
db.counters[t.Set] = make(map[string]map[string]int)
|
||||||
|
}
|
||||||
|
if db.counters[t.Set][t.Case] == nil {
|
||||||
|
db.counters[t.Set][t.Case] = make(map[string]int)
|
||||||
|
}
|
||||||
|
db.counters[t.Set][t.Case]["failed"] += 1
|
||||||
|
db.NumberOfTests += 1
|
||||||
|
}
|
||||||
|
for _, t := range unresolvedTests {
|
||||||
|
if db.counters[t.Set] == nil {
|
||||||
|
db.counters[t.Set] = make(map[string]map[string]int)
|
||||||
|
}
|
||||||
|
if db.counters[t.Set][t.Case] == nil {
|
||||||
|
db.counters[t.Set][t.Case] = make(map[string]int)
|
||||||
|
}
|
||||||
|
if (ignoreUnresolved || nonBlockedAsPassed) && !isFalsePositiveTest(t.Set) {
|
||||||
|
db.counters[t.Set][t.Case]["passed"]++
|
||||||
|
} else {
|
||||||
|
db.counters[t.Set][t.Case]["blocked"]++
|
||||||
|
}
|
||||||
|
db.NumberOfTests += 1
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
},
|
||||||
|
func(db *DB) ([]*Info, []*Info, []*Info, []*Info) {
|
||||||
|
return db.passedTests, db.blockedTests, db.failedTests, db.naTests
|
||||||
|
},
|
||||||
|
GenInfoSlice(),
|
||||||
|
GenInfoSlice(),
|
||||||
|
GenInfoSlice(),
|
||||||
|
GenInfoSlice(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenSetName() gopter.Gen {
|
||||||
|
return func(parameters *gopter.GenParameters) *gopter.GenResult {
|
||||||
|
setName := fmt.Sprintf("setName-%d", parameters.Rng.Intn(10))
|
||||||
|
if rand.Intn(2) == 1 {
|
||||||
|
setName = "false-" + setName
|
||||||
|
}
|
||||||
|
return gopter.NewGenResult(setName, gopter.NoShrinker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenCaseName() gopter.Gen {
|
||||||
|
return func(parameters *gopter.GenParameters) *gopter.GenResult {
|
||||||
|
caseName := fmt.Sprintf("caseName-%d", parameters.Rng.Intn(10))
|
||||||
|
return gopter.NewGenResult(caseName, gopter.NoShrinker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenTestInfo() gopter.Gen {
|
||||||
|
return gopter.DeriveGen(
|
||||||
|
func(setName, caseName string) *Info {
|
||||||
|
return &Info{
|
||||||
|
Set: setName,
|
||||||
|
Case: caseName,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
func(i *Info) (string, string) {
|
||||||
|
return i.Set, i.Case
|
||||||
|
},
|
||||||
|
GenSetName(),
|
||||||
|
GenCaseName(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenInfoSlice() gopter.Gen {
|
||||||
|
return gen.SliceOf(GenTestInfo())
|
||||||
|
}
|
||||||
|
|
||||||
|
func BoolGenerator(b bool) gopter.Gen {
|
||||||
|
return func(parameters *gopter.GenParameters) *gopter.GenResult {
|
||||||
|
return gopter.NewGenResult(b, gopter.NoShrinker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatisticsCalculation(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
apiSecTruePosBypassesNum int
|
||||||
|
apiSecTruePosBlockedNum int
|
||||||
|
|
||||||
|
apiSecTrueNegBypassesNum int
|
||||||
|
apiSecTrueNegBlockedNum int
|
||||||
|
|
||||||
|
appSecTruePosBypassesNum int
|
||||||
|
appSecTruePosBlockedNum int
|
||||||
|
|
||||||
|
appSecTrueNegBypassesNum int
|
||||||
|
appSecTrueNegBlockedNum int
|
||||||
|
}{
|
||||||
|
{0, 0, 0, 0, 0, 0, 0, 0},
|
||||||
|
{rand.Int()%500 + 1, rand.Int()%500 + 1, rand.Int()%500 + 1, rand.Int()%500 + 1, rand.Int()%500 + 1, rand.Int()%500 + 1, rand.Int()%500 + 1, rand.Int()%500 + 1},
|
||||||
|
{rand.Int()%500 + 1, 0, 0, 0, 0, 0, 0, 0},
|
||||||
|
{0, rand.Int()%500 + 1, 0, 0, 0, 0, 0, 0},
|
||||||
|
{rand.Int()%500 + 1, rand.Int()%500 + 1, 0, 0, 0, 0, 0, 0},
|
||||||
|
{0, 0, rand.Int()%500 + 1, 0, 0, 0, 0, 0},
|
||||||
|
{0, 0, 0, rand.Int()%500 + 1, 0, 0, 0, 0},
|
||||||
|
{0, 0, rand.Int()%500 + 1, rand.Int()%500 + 1, 0, 0, 0, 0},
|
||||||
|
{0, 0, 0, 0, rand.Int()%500 + 1, 0, 0, 0},
|
||||||
|
{0, 0, 0, 0, 0, rand.Int()%500 + 1, 0, 0},
|
||||||
|
{0, 0, 0, 0, rand.Int()%500 + 1, rand.Int()%500 + 1, 0, 0},
|
||||||
|
{0, 0, 0, 0, 0, 0, rand.Int()%500 + 1, 0},
|
||||||
|
{0, 0, 0, 0, 0, 0, 0, rand.Int()%500 + 1},
|
||||||
|
{0, 0, 0, 0, 0, 0, rand.Int()%500 + 1, rand.Int()%500 + 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []*Case{
|
||||||
|
{Set: ""},
|
||||||
|
{Set: "false"},
|
||||||
|
{Set: "api"},
|
||||||
|
{Set: "api-false"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
db, err := NewDB(cases)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < tc.apiSecTruePosBypassesNum; i++ {
|
||||||
|
db.UpdatePassedTests(&Info{Set: "api"})
|
||||||
|
}
|
||||||
|
for i := 0; i < tc.apiSecTruePosBlockedNum; i++ {
|
||||||
|
db.UpdateBlockedTests(&Info{Set: "api"})
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < tc.apiSecTrueNegBypassesNum; i++ {
|
||||||
|
db.UpdatePassedTests(&Info{Set: "api-false"})
|
||||||
|
}
|
||||||
|
for i := 0; i < tc.apiSecTrueNegBlockedNum; i++ {
|
||||||
|
db.UpdateBlockedTests(&Info{Set: "api-false"})
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < tc.appSecTruePosBypassesNum; i++ {
|
||||||
|
db.UpdatePassedTests(&Info{})
|
||||||
|
}
|
||||||
|
for i := 0; i < tc.appSecTruePosBlockedNum; i++ {
|
||||||
|
db.UpdateBlockedTests(&Info{})
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < tc.appSecTrueNegBypassesNum; i++ {
|
||||||
|
db.UpdatePassedTests(&Info{Set: "false"})
|
||||||
|
}
|
||||||
|
for i := 0; i < tc.appSecTrueNegBlockedNum; i++ {
|
||||||
|
db.UpdateBlockedTests(&Info{Set: "false"})
|
||||||
|
}
|
||||||
|
|
||||||
|
stat := db.GetStatistics(false, false)
|
||||||
|
|
||||||
|
sum := 0.0
|
||||||
|
div := 0
|
||||||
|
|
||||||
|
apiSecTruePosNum := tc.apiSecTruePosBypassesNum + tc.apiSecTruePosBlockedNum
|
||||||
|
apiSecTruePosPercentage := CalculatePercentage(tc.apiSecTruePosBlockedNum, apiSecTruePosNum)
|
||||||
|
if apiSecTruePosNum == 0 {
|
||||||
|
apiSecTruePosPercentage = -1.0
|
||||||
|
} else {
|
||||||
|
div++
|
||||||
|
sum += apiSecTruePosPercentage
|
||||||
|
}
|
||||||
|
|
||||||
|
apiSecTrueNegNum := tc.apiSecTrueNegBypassesNum + tc.apiSecTrueNegBlockedNum
|
||||||
|
apiSecTrueNegPercentage := CalculatePercentage(tc.apiSecTrueNegBypassesNum, apiSecTrueNegNum)
|
||||||
|
if apiSecTrueNegNum == 0 {
|
||||||
|
apiSecTrueNegPercentage = -1.0
|
||||||
|
} else {
|
||||||
|
div++
|
||||||
|
sum += apiSecTrueNegPercentage
|
||||||
|
}
|
||||||
|
|
||||||
|
apiSecAverage := 0.0
|
||||||
|
if div == 0 {
|
||||||
|
apiSecAverage = -1.0
|
||||||
|
} else {
|
||||||
|
if tc.apiSecTruePosBlockedNum != 0 {
|
||||||
|
apiSecAverage = Round(sum / float64(div))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(tc)
|
||||||
|
|
||||||
|
if stat.Score.ApiSec.TruePositive != apiSecTruePosPercentage {
|
||||||
|
t.Fatalf("ApiSec.TruePositive: want %#v, got %#v", apiSecTruePosPercentage, stat.Score.ApiSec.TruePositive)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat.Score.ApiSec.TrueNegative != apiSecTrueNegPercentage {
|
||||||
|
t.Fatalf("ApiSec.TrueNegative: want %#v, got %#v", apiSecTrueNegPercentage, stat.Score.ApiSec.TrueNegative)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat.Score.ApiSec.Average != apiSecAverage {
|
||||||
|
t.Fatalf("ApiSec.Average: want %#v, got %#v", apiSecAverage, stat.Score.ApiSec.Average)
|
||||||
|
}
|
||||||
|
|
||||||
|
sum = 0.0
|
||||||
|
div = 0
|
||||||
|
|
||||||
|
appSecTruePosNum := tc.appSecTruePosBypassesNum + tc.appSecTruePosBlockedNum
|
||||||
|
appSecTruePosPercentage := CalculatePercentage(tc.appSecTruePosBlockedNum, appSecTruePosNum)
|
||||||
|
if appSecTruePosNum == 0 {
|
||||||
|
appSecTruePosPercentage = -1.0
|
||||||
|
} else {
|
||||||
|
div++
|
||||||
|
sum += appSecTruePosPercentage
|
||||||
|
}
|
||||||
|
|
||||||
|
appSecTrueNegNum := tc.appSecTrueNegBypassesNum + tc.appSecTrueNegBlockedNum
|
||||||
|
appSecTrueNegPercentage := CalculatePercentage(tc.appSecTrueNegBypassesNum, appSecTrueNegNum)
|
||||||
|
if appSecTrueNegNum == 0 {
|
||||||
|
appSecTrueNegPercentage = -1.0
|
||||||
|
} else {
|
||||||
|
div++
|
||||||
|
sum += appSecTrueNegPercentage
|
||||||
|
}
|
||||||
|
|
||||||
|
appSecAverage := 0.0
|
||||||
|
if div == 0 {
|
||||||
|
appSecAverage = -1.0
|
||||||
|
} else {
|
||||||
|
if tc.appSecTruePosBlockedNum != 0 {
|
||||||
|
appSecAverage = Round(sum / float64(div))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat.Score.AppSec.TruePositive != appSecTruePosPercentage {
|
||||||
|
t.Fatalf("AppSec.TruePositive: want %#v, got %#v", appSecTruePosPercentage, stat.Score.AppSec.TruePositive)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat.Score.AppSec.TrueNegative != appSecTrueNegPercentage {
|
||||||
|
t.Fatalf("AppSec.TrueNegative: want %#v, got %#v", appSecTrueNegPercentage, stat.Score.AppSec.TrueNegative)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat.Score.AppSec.Average != appSecAverage {
|
||||||
|
t.Fatalf("AppSec.Average: want %#v, got %#v", appSecAverage, stat.Score.AppSec.Average)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
AttackSurface/src/gotestwaf/internal/dnscache/dns_cache.go
Normal file
23
AttackSurface/src/gotestwaf/internal/dnscache/dns_cache.go
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
package dnscache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/pkg/dnscache"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dnsRefreshTime = 30 * time.Minute
|
||||||
|
dnsLookupTimeout = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewDNSCache(logger *logrus.Logger) (*dnscache.Resolver, error) {
|
||||||
|
dnsResolver, err := dnscache.New(dnsRefreshTime, dnsLookupTimeout, logger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dnsResolver, nil
|
||||||
|
}
|
||||||
23
AttackSurface/src/gotestwaf/internal/helpers/email.go
Normal file
23
AttackSurface/src/gotestwaf/internal/helpers/email.go
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mcnijman/go-emailaddress"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const MaxEmailLength = 254
|
||||||
|
|
||||||
|
func ValidateEmail(email string) (string, error) {
|
||||||
|
parsedEmail, err := emailaddress.Parse(email)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "couldn't parse email")
|
||||||
|
}
|
||||||
|
|
||||||
|
email = parsedEmail.String()
|
||||||
|
|
||||||
|
if len(email) > MaxEmailLength {
|
||||||
|
return "", errors.New("email too long")
|
||||||
|
}
|
||||||
|
|
||||||
|
return email, nil
|
||||||
|
}
|
||||||
56
AttackSurface/src/gotestwaf/internal/helpers/file_move.go
Normal file
56
AttackSurface/src/gotestwaf/internal/helpers/file_move.go
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileMove moves a file from a source path to a destination path.
|
||||||
|
// This must be used across the codebase for compatibility with Docker volumes
|
||||||
|
// and Golang (fixes Invalid cross-device link when using [os.Rename])
|
||||||
|
func FileMove(sourcePath, destPath string) error {
|
||||||
|
// check the source file is exist
|
||||||
|
sourceFileStat, err := os.Stat(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the destination file
|
||||||
|
destFileStat, err := os.Stat(destPath)
|
||||||
|
if err == nil {
|
||||||
|
// return error if the destination file is the same file as source one
|
||||||
|
if sourcePath == destPath || os.SameFile(sourceFileStat, destFileStat) {
|
||||||
|
return fmt.Errorf("files %s and %s are the same", sourcePath, destPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputFile, err := os.Open(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFile, err := os.Create(destPath)
|
||||||
|
if err != nil {
|
||||||
|
inputFile.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(outputFile, inputFile)
|
||||||
|
inputFile.Close()
|
||||||
|
outputFile.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errRem := os.Remove(destPath); errRem != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"unable to os.Remove error: %s after io.Copy error: %s",
|
||||||
|
errRem,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.Remove(sourcePath)
|
||||||
|
}
|
||||||
199
AttackSurface/src/gotestwaf/internal/helpers/file_move_test.go
Normal file
199
AttackSurface/src/gotestwaf/internal/helpers/file_move_test.go
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testDir string
|
||||||
|
|
||||||
|
func createTestFile(dst string, content string) error {
|
||||||
|
f, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
_, err = f.WriteString(content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
var err error
|
||||||
|
testDir, err = os.MkdirTemp("", "gtw_test_dir")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Couldn't create directory for test content:", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
exitVal := m.Run()
|
||||||
|
|
||||||
|
err = os.RemoveAll(testDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Couldn't remove directory for test content:", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(exitVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileMoveSourceFileNotExist(t *testing.T) {
|
||||||
|
srcFile := filepath.Join(testDir, "gtw_test_file_not_exist.txt")
|
||||||
|
dstFile := filepath.Join(testDir, "gtw_test_file_not_exist_dst.txt")
|
||||||
|
|
||||||
|
err := FileMove(srcFile, dstFile)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("try to move file that is not existed, err must not be nil")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Errorf("err must be os.ErrNotExist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileMoveSourceDestinationTheSamePath(t *testing.T) {
|
||||||
|
srcFile := filepath.Join(testDir, "gtw_test_file.txt")
|
||||||
|
dstFile := srcFile
|
||||||
|
|
||||||
|
err := createTestFile(srcFile, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("couldn't create test file: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = os.Remove(srcFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("couldn't remove test file: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = FileMove(srcFile, dstFile)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("the dstFile is the same as the srcFile, couln't move srcFile to dstFile, err must not be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileMoveSourceDestinationTheSameFile(t *testing.T) {
|
||||||
|
srcFile := filepath.Join(testDir, "gtw_test_file.txt")
|
||||||
|
dstFile := filepath.Join(testDir, "gtw_test_file_link.txt")
|
||||||
|
|
||||||
|
err := createTestFile(srcFile, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("couldn't create test file: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = os.Remove(srcFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("couldn't remove test file: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = os.Link(srcFile, dstFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("couldn't create link for test file: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = os.Remove(dstFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("couldn't remove link for test file: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = FileMove(srcFile, dstFile)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("the dstFile is the link to the srcFile, couln't move srcFile to dstFile, err must not be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileMove(t *testing.T) {
|
||||||
|
srcFile := filepath.Join(testDir, "gtw_test_file_1.txt")
|
||||||
|
dstFile := filepath.Join(testDir, "gtw_test_file_2.txt")
|
||||||
|
fileContent := "test1"
|
||||||
|
|
||||||
|
err := createTestFile(srcFile, fileContent)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("couldn't create test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
defer func() {
|
||||||
|
os.Remove(srcFile)
|
||||||
|
os.Remove(dstFile)
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = FileMove(srcFile, dstFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("err is not nil: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = os.Stat(srcFile)
|
||||||
|
if err == nil || !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Errorf("file %s must not exist", srcFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = os.Stat(dstFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("file %s must exist", srcFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
dstFileContent, err := os.ReadFile(dstFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("couldn't reade dstFile content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileContent != string(dstFileContent) {
|
||||||
|
t.Errorf("dstFile content is not the same as srcFile content: '%s' != '%s'", dstFileContent, fileContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileMoveReplace(t *testing.T) {
|
||||||
|
srcFile := filepath.Join(testDir, "gtw_test_file_1.txt")
|
||||||
|
dstFile := filepath.Join(testDir, "gtw_test_file_2.txt")
|
||||||
|
srcFileContent := "test1"
|
||||||
|
dstFileContent := "test2"
|
||||||
|
|
||||||
|
err := createTestFile(srcFile, srcFileContent)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("couldn't create test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = createTestFile(dstFile, dstFileContent)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("couldn't create test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
defer func() {
|
||||||
|
os.Remove(srcFile)
|
||||||
|
os.Remove(dstFile)
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = FileMove(srcFile, dstFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("err is not nil: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = os.Stat(srcFile)
|
||||||
|
if err == nil || !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Errorf("file %s must not exist", srcFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = os.Stat(dstFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("file %s must exist", srcFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileContent, err := os.ReadFile(dstFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("couldn't reade dstFile content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if srcFileContent != string(fileContent) {
|
||||||
|
t.Errorf("dstFile content is not the same as srcFile content: '%s' != '%s'", fileContent, srcFileContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
5
AttackSurface/src/gotestwaf/internal/helpers/hash.go
Normal file
5
AttackSurface/src/gotestwaf/internal/helpers/hash.go
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
package helpers
|
||||||
|
|
||||||
|
type Hash interface {
|
||||||
|
Hash() []byte
|
||||||
|
}
|
||||||
55
AttackSurface/src/gotestwaf/internal/helpers/url.go
Normal file
55
AttackSurface/src/gotestwaf/internal/helpers/url.go
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetTargetURL returns *url.URL with empty path, query and fragments parts.
|
||||||
|
func GetTargetURL(reqURL *url.URL) *url.URL {
|
||||||
|
targetURL := *reqURL
|
||||||
|
|
||||||
|
targetURL.Path = ""
|
||||||
|
targetURL.RawPath = ""
|
||||||
|
targetURL.ForceQuery = false
|
||||||
|
targetURL.RawQuery = ""
|
||||||
|
targetURL.Fragment = ""
|
||||||
|
targetURL.RawFragment = ""
|
||||||
|
|
||||||
|
return &targetURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTargetURLStr returns *url.URL with empty path, query and fragments parts
|
||||||
|
// as a string.
|
||||||
|
func GetTargetURLStr(reqURL *url.URL) string {
|
||||||
|
targetURL := GetTargetURL(reqURL)
|
||||||
|
|
||||||
|
return targetURL.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HostPortFromUrl returns is TLS required flag and host:port string.
|
||||||
|
func HostPortFromUrl(wafURL string, port uint16) (isTLS bool, hostPort string, err error) {
|
||||||
|
urlParse, err := url.Parse(wafURL)
|
||||||
|
if err != nil {
|
||||||
|
return isTLS, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
host, _, err := net.SplitHostPort(urlParse.Host)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "port") {
|
||||||
|
host = urlParse.Host
|
||||||
|
} else {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
host = net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||||
|
|
||||||
|
if urlParse.Scheme == "https" {
|
||||||
|
isTLS = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return isTLS, host, nil
|
||||||
|
}
|
||||||
14
AttackSurface/src/gotestwaf/internal/helpers/utils.go
Normal file
14
AttackSurface/src/gotestwaf/internal/helpers/utils.go
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
package helpers
|
||||||
|
|
||||||
|
// DeepCopyMap is a generic function to copy a map with any key and value types.
|
||||||
|
func DeepCopyMap[K comparable, V any](original map[K]V) map[K]V {
|
||||||
|
// Create a new map to hold the copy
|
||||||
|
mapCopy := make(map[K]V)
|
||||||
|
|
||||||
|
// Iterate over the original map and copy each key-value pair to the new map
|
||||||
|
for key, value := range original {
|
||||||
|
mapCopy[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapCopy
|
||||||
|
}
|
||||||
74
AttackSurface/src/gotestwaf/internal/openapi/helpers.go
Normal file
74
AttackSurface/src/gotestwaf/internal/openapi/helpers.go
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
package openapi
|
||||||
|
|
||||||
|
import "math/rand"
|
||||||
|
|
||||||
|
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
|
|
||||||
|
// genRandomInt generates a random integer within the given bounds.
|
||||||
|
func genRandomInt(min, max *float64, exclusiveMin, exclusiveMax bool) int {
|
||||||
|
minValue := 0
|
||||||
|
maxValue := defaultMaxInt
|
||||||
|
|
||||||
|
if min != nil {
|
||||||
|
minValue = int(*min)
|
||||||
|
}
|
||||||
|
if max != nil {
|
||||||
|
maxValue = int(*max)
|
||||||
|
}
|
||||||
|
|
||||||
|
if exclusiveMin {
|
||||||
|
minValue++
|
||||||
|
}
|
||||||
|
if !exclusiveMax {
|
||||||
|
maxValue++
|
||||||
|
}
|
||||||
|
|
||||||
|
randInt := minValue + rand.Intn(maxValue-minValue)
|
||||||
|
|
||||||
|
return randInt
|
||||||
|
}
|
||||||
|
|
||||||
|
// genRandomFloat generates a random float within the given bounds.
|
||||||
|
func genRandomFloat(min, max *float64, exclusiveMin, exclusiveMax bool) float64 {
|
||||||
|
minValue := float64(0)
|
||||||
|
maxValue := float64(defaultMaxInt)
|
||||||
|
|
||||||
|
if min != nil {
|
||||||
|
minValue = *min
|
||||||
|
}
|
||||||
|
if max != nil {
|
||||||
|
maxValue = *max
|
||||||
|
}
|
||||||
|
|
||||||
|
if exclusiveMin {
|
||||||
|
minValue++
|
||||||
|
}
|
||||||
|
if !exclusiveMax {
|
||||||
|
maxValue++
|
||||||
|
}
|
||||||
|
|
||||||
|
randFloat := minValue + float64(rand.Intn(int(maxValue-minValue)))
|
||||||
|
|
||||||
|
return randFloat
|
||||||
|
}
|
||||||
|
|
||||||
|
// genRandomString generates a random string of the right size.
|
||||||
|
func genRandomString(minLength, maxLength uint64) string {
|
||||||
|
if minLength < defaultStringSize {
|
||||||
|
minLength = defaultStringSize
|
||||||
|
}
|
||||||
|
|
||||||
|
randLength := int(minLength) + rand.Intn(int(maxLength-minLength+1))
|
||||||
|
|
||||||
|
b := make([]rune, randLength)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// genRandomPlaceholder generates a random placeholder with fixed size.
|
||||||
|
func genRandomPlaceholder() string {
|
||||||
|
return genRandomString(defaultPlaceholderSize, defaultPlaceholderSize)
|
||||||
|
}
|
||||||
33
AttackSurface/src/gotestwaf/internal/openapi/loader.go
Normal file
33
AttackSurface/src/gotestwaf/internal/openapi/loader.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package openapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/getkin/kin-openapi/openapi3"
|
||||||
|
"github.com/getkin/kin-openapi/routers"
|
||||||
|
routers_legacy "github.com/getkin/kin-openapi/routers/legacy"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadOpenAPISpec loads an openAPI file, parses it and validates data.
|
||||||
|
func LoadOpenAPISpec(ctx context.Context, location string) (*openapi3.T, routers.Router, error) {
|
||||||
|
loader := openapi3.NewLoader()
|
||||||
|
loader.IsExternalRefsAllowed = true
|
||||||
|
|
||||||
|
doc, err := loader.LoadFromFile(location)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "couldn't load OpenAPI file")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = doc.Validate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "couldn't validate OpenAPI spec")
|
||||||
|
}
|
||||||
|
|
||||||
|
router, err := routers_legacy.NewRouter(doc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "couldn't create router from OpenAPI spec")
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc, router, nil
|
||||||
|
}
|
||||||
400
AttackSurface/src/gotestwaf/internal/openapi/parameters.go
Normal file
400
AttackSurface/src/gotestwaf/internal/openapi/parameters.go
Normal file
|
|
@ -0,0 +1,400 @@
|
||||||
|
package openapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/getkin/kin-openapi/openapi3"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultPlaceholderSize = 16
|
||||||
|
defaultMaxInt = 10000
|
||||||
|
defaultStringSize = 16
|
||||||
|
|
||||||
|
defaultQueryParameterArrayBinder = ","
|
||||||
|
spaceQueryParameterArrayBinder = "%20"
|
||||||
|
pipeQueryParameterArrayBinder = "|"
|
||||||
|
)
|
||||||
|
|
||||||
|
// allParameters contains names of parameters and their value placeholders.
|
||||||
|
type allParameters struct {
|
||||||
|
pathParameters map[string]*parameterSpec
|
||||||
|
queryParameters map[string]*parameterSpec
|
||||||
|
headers map[string]*parameterSpec
|
||||||
|
|
||||||
|
supportedPlaceholders map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parameterSpec contains a specific value for any parameter and
|
||||||
|
// length limits for string parameters.
|
||||||
|
type parameterSpec struct {
|
||||||
|
paramType string
|
||||||
|
value string
|
||||||
|
minLength uint64
|
||||||
|
maxLength uint64
|
||||||
|
explode bool
|
||||||
|
paramSpec map[string]*parameterSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseParameters returns information about parameters in path, query and headers.
|
||||||
|
func parseParameters(parameters openapi3.Parameters) (*allParameters, error) {
|
||||||
|
pathParams := make(map[string]*parameterSpec)
|
||||||
|
queryParams := make(map[string]*parameterSpec)
|
||||||
|
headers := make(map[string]*parameterSpec)
|
||||||
|
supportedPlaceholders := make(map[string]interface{})
|
||||||
|
|
||||||
|
if parameters != nil {
|
||||||
|
for _, p := range parameters {
|
||||||
|
switch p.Value.In {
|
||||||
|
case openapi3.ParameterInPath:
|
||||||
|
param, spec, err := parsePathParameter(p.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pathParams[param] = spec
|
||||||
|
|
||||||
|
if spec.paramType == openapi3.TypeString {
|
||||||
|
supportedPlaceholders[urlPathPlaceholder] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
case openapi3.ParameterInQuery:
|
||||||
|
param, spec, err := parseQueryParameter(p.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParams[param] = spec
|
||||||
|
|
||||||
|
if spec.paramType == openapi3.TypeString {
|
||||||
|
supportedPlaceholders[urlParamPlaceholder] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
case openapi3.ParameterInHeader:
|
||||||
|
header, spec, err := parseHeaderParameter(p.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
headers[header] = spec
|
||||||
|
|
||||||
|
if spec.paramType == openapi3.TypeString {
|
||||||
|
supportedPlaceholders[headerPlaceholder] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported parameter place: %s", openapi3.ParameterInCookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params := &allParameters{
|
||||||
|
pathParameters: pathParams,
|
||||||
|
queryParameters: queryParams,
|
||||||
|
headers: headers,
|
||||||
|
|
||||||
|
supportedPlaceholders: supportedPlaceholders,
|
||||||
|
}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHeaderParameter returns the path parameter name, path parameter value
|
||||||
|
// placeholder and value type.
|
||||||
|
func parsePathParameter(parameter *openapi3.Parameter) (paramName string, spec *parameterSpec, err error) {
|
||||||
|
paramName = "{" + parameter.Name + "}"
|
||||||
|
spec = ¶meterSpec{}
|
||||||
|
|
||||||
|
style := parameter.Style
|
||||||
|
if style != "" && style != openapi3.SerializationSimple {
|
||||||
|
return "", nil, fmt.Errorf("unsupported path parameter style: %s", style)
|
||||||
|
}
|
||||||
|
|
||||||
|
schema := parameter.Schema.Value
|
||||||
|
|
||||||
|
paramType := ""
|
||||||
|
if schema.Type != nil && len(*schema.Type) > 0 {
|
||||||
|
paramType = (*schema.Type)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
spec.paramType = paramType
|
||||||
|
switch paramType {
|
||||||
|
case openapi3.TypeNumber:
|
||||||
|
randFloat := genRandomFloat(schema.Min, schema.Max, schema.ExclusiveMin, schema.ExclusiveMax)
|
||||||
|
spec.value = fmt.Sprintf("%f", randFloat)
|
||||||
|
|
||||||
|
case openapi3.TypeInteger:
|
||||||
|
randInt := genRandomInt(schema.Min, schema.Max, schema.ExclusiveMin, schema.ExclusiveMax)
|
||||||
|
spec.value = fmt.Sprintf("%d", randInt)
|
||||||
|
|
||||||
|
case openapi3.TypeString:
|
||||||
|
spec.minLength = schema.MinLength
|
||||||
|
if schema.MaxLength == nil {
|
||||||
|
spec.maxLength = math.MaxUint64
|
||||||
|
} else {
|
||||||
|
spec.maxLength = *schema.MaxLength
|
||||||
|
}
|
||||||
|
spec.value = genRandomString(spec.minLength, spec.minLength+defaultStringSize)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", nil, fmt.Errorf("unsupported path parameter type: %s", schema.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHeaderParameter returns the query parameter name, query parameter value
|
||||||
|
// placeholder and value type.
|
||||||
|
func parseQueryParameter(parameter *openapi3.Parameter) (paramName string, spec *parameterSpec, err error) {
|
||||||
|
paramName = parameter.Name
|
||||||
|
spec = ¶meterSpec{}
|
||||||
|
|
||||||
|
style := parameter.Style
|
||||||
|
if style == "" {
|
||||||
|
style = openapi3.SerializationForm
|
||||||
|
}
|
||||||
|
if style != openapi3.SerializationForm &&
|
||||||
|
style != openapi3.SerializationSpaceDelimited &&
|
||||||
|
style != openapi3.SerializationPipeDelimited &&
|
||||||
|
style != openapi3.SerializationDeepObject {
|
||||||
|
return "", nil, fmt.Errorf("unsupported query parameter style: %s", style)
|
||||||
|
}
|
||||||
|
|
||||||
|
var schema *openapi3.Schema
|
||||||
|
var isJSON bool
|
||||||
|
|
||||||
|
if parameter.Schema != nil {
|
||||||
|
schema = parameter.Schema.Value
|
||||||
|
} else if parameter.Content != nil {
|
||||||
|
if _, ok := parameter.Content[jsonContentType]; !ok {
|
||||||
|
return "", nil, fmt.Errorf("unsupported content type in content of query parameter specification")
|
||||||
|
}
|
||||||
|
schema = parameter.Content[jsonContentType].Schema.Value
|
||||||
|
isJSON = true
|
||||||
|
} else {
|
||||||
|
return "", nil, fmt.Errorf("neither schema nor content not found in query parameter specification")
|
||||||
|
}
|
||||||
|
|
||||||
|
paramType := ""
|
||||||
|
if schema.Type != nil && len(*schema.Type) > 0 {
|
||||||
|
paramType = (*schema.Type)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
spec.paramType = paramType
|
||||||
|
switch paramType {
|
||||||
|
case openapi3.TypeNumber:
|
||||||
|
randFloat := genRandomFloat(schema.Min, schema.Max, schema.ExclusiveMin, schema.ExclusiveMax)
|
||||||
|
spec.value = fmt.Sprintf("%f", randFloat)
|
||||||
|
|
||||||
|
case openapi3.TypeInteger:
|
||||||
|
randInt := genRandomInt(schema.Min, schema.Max, schema.ExclusiveMin, schema.ExclusiveMax)
|
||||||
|
spec.value = fmt.Sprintf("%d", randInt)
|
||||||
|
|
||||||
|
case openapi3.TypeString:
|
||||||
|
spec.minLength = schema.MinLength
|
||||||
|
if schema.MaxLength == nil {
|
||||||
|
spec.maxLength = math.MaxUint64
|
||||||
|
} else {
|
||||||
|
spec.maxLength = *schema.MaxLength
|
||||||
|
}
|
||||||
|
spec.value = genRandomString(spec.minLength, spec.minLength+defaultStringSize)
|
||||||
|
|
||||||
|
case openapi3.TypeArray:
|
||||||
|
items := schema.Items.Value
|
||||||
|
paramType = ""
|
||||||
|
if schema.Type != nil && len(*schema.Type) > 0 {
|
||||||
|
paramType = (*items.Type)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
spec.paramType = paramType
|
||||||
|
switch paramType {
|
||||||
|
case openapi3.TypeNumber:
|
||||||
|
randFloat := genRandomFloat(schema.Min, schema.Max, schema.ExclusiveMin, schema.ExclusiveMax)
|
||||||
|
spec.value = fmt.Sprintf("%f", randFloat)
|
||||||
|
|
||||||
|
case openapi3.TypeInteger:
|
||||||
|
randInt := genRandomInt(schema.Min, schema.Max, schema.ExclusiveMin, schema.ExclusiveMax)
|
||||||
|
spec.value = fmt.Sprintf("%d", randInt)
|
||||||
|
|
||||||
|
case openapi3.TypeString:
|
||||||
|
spec.minLength = schema.MinLength
|
||||||
|
if schema.MaxLength == nil {
|
||||||
|
spec.maxLength = math.MaxUint64
|
||||||
|
} else {
|
||||||
|
spec.maxLength = *schema.MaxLength
|
||||||
|
}
|
||||||
|
spec.value = genRandomString(spec.minLength, spec.minLength+defaultStringSize)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", nil, fmt.Errorf("unsupported type of items in query parameter array: %s", items.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.MinItems > 1 {
|
||||||
|
if parameter.Explode != nil {
|
||||||
|
spec.explode = *parameter.Explode
|
||||||
|
}
|
||||||
|
|
||||||
|
items := []string{spec.value}
|
||||||
|
|
||||||
|
for i := schema.MinItems; i > 1; i-- {
|
||||||
|
items = append(items, spec.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if spec.explode {
|
||||||
|
prefix := paramName + "="
|
||||||
|
binder := "&" + paramName + "="
|
||||||
|
|
||||||
|
// spec.value = "paramName=item1¶mName=item2¶mName=item3"
|
||||||
|
spec.value = prefix + strings.Join(items, binder)
|
||||||
|
} else {
|
||||||
|
binder := defaultQueryParameterArrayBinder
|
||||||
|
if style == openapi3.SerializationSpaceDelimited {
|
||||||
|
binder = spaceQueryParameterArrayBinder
|
||||||
|
} else if style == openapi3.SerializationPipeDelimited {
|
||||||
|
binder = pipeQueryParameterArrayBinder
|
||||||
|
}
|
||||||
|
|
||||||
|
spec.value = strings.Join(items, binder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case openapi3.TypeObject:
|
||||||
|
value, strAvailable, paramSpec, err := schemaToMap("", schema, false)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, errors.Wrap(err, "couldn't parse query parameter object")
|
||||||
|
}
|
||||||
|
|
||||||
|
if isJSON {
|
||||||
|
jsonValue, err := jsonMarshal(value)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, errors.Wrap(err, "couldn't marshal query parameter object to JSON")
|
||||||
|
}
|
||||||
|
spec.value = url.QueryEscape(jsonValue)
|
||||||
|
} else {
|
||||||
|
parts := queryParamStructParts(paramName, value)
|
||||||
|
spec.value = strings.Join(parts, "&")
|
||||||
|
spec.explode = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strAvailable {
|
||||||
|
spec.paramSpec = paramSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
case openapi3.TypeBoolean:
|
||||||
|
spec.value = "false"
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", nil, fmt.Errorf("unsupported query parameter type: %s", schema.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHeaderParameter returns the header name, header value placeholder and
|
||||||
|
// value type.
|
||||||
|
func parseHeaderParameter(parameter *openapi3.Parameter) (paramName string, spec *parameterSpec, err error) {
|
||||||
|
paramName = parameter.Name
|
||||||
|
spec = ¶meterSpec{}
|
||||||
|
|
||||||
|
style := parameter.Style
|
||||||
|
if style != "" &&
|
||||||
|
style != openapi3.SerializationSimple {
|
||||||
|
return "", nil, fmt.Errorf("unsupported header parameter style: %s", style)
|
||||||
|
}
|
||||||
|
|
||||||
|
var schema *openapi3.Schema
|
||||||
|
var isJSON bool
|
||||||
|
|
||||||
|
if parameter.Schema != nil {
|
||||||
|
schema = parameter.Schema.Value
|
||||||
|
} else if parameter.Content != nil {
|
||||||
|
if _, ok := parameter.Content[jsonContentType]; !ok {
|
||||||
|
return "", nil, fmt.Errorf("unsupported content type in content of header specification")
|
||||||
|
}
|
||||||
|
schema = parameter.Content[jsonContentType].Schema.Value
|
||||||
|
isJSON = true
|
||||||
|
} else {
|
||||||
|
return "", nil, fmt.Errorf("neither schema nor content not found in header specification")
|
||||||
|
}
|
||||||
|
|
||||||
|
paramType := ""
|
||||||
|
if schema.Type != nil && len(*schema.Type) > 0 {
|
||||||
|
paramType = (*schema.Type)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
spec.paramType = paramType
|
||||||
|
switch paramType {
|
||||||
|
case openapi3.TypeNumber:
|
||||||
|
randFloat := genRandomFloat(schema.Min, schema.Max, schema.ExclusiveMin, schema.ExclusiveMax)
|
||||||
|
spec.value = fmt.Sprintf("%f", randFloat)
|
||||||
|
|
||||||
|
case openapi3.TypeInteger:
|
||||||
|
randInt := genRandomInt(schema.Min, schema.Max, schema.ExclusiveMin, schema.ExclusiveMax)
|
||||||
|
spec.value = fmt.Sprintf("%d", randInt)
|
||||||
|
|
||||||
|
case openapi3.TypeString:
|
||||||
|
spec.minLength = schema.MinLength
|
||||||
|
if schema.MaxLength == nil {
|
||||||
|
spec.maxLength = math.MaxUint64
|
||||||
|
} else {
|
||||||
|
spec.maxLength = *schema.MaxLength
|
||||||
|
}
|
||||||
|
spec.value = genRandomString(spec.minLength, spec.minLength+defaultStringSize)
|
||||||
|
|
||||||
|
case openapi3.TypeObject:
|
||||||
|
value, strAvailable, paramSpec, err := schemaToMap("", schema, false)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, errors.Wrap(err, "couldn't parse header object")
|
||||||
|
}
|
||||||
|
|
||||||
|
if isJSON {
|
||||||
|
jsonValue, err := jsonMarshal(value)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, errors.Wrap(err, "couldn't marshal header object to JSON")
|
||||||
|
}
|
||||||
|
spec.value = url.QueryEscape(jsonValue)
|
||||||
|
} else {
|
||||||
|
return "", nil, fmt.Errorf("unsupported content type in content of header specification")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strAvailable {
|
||||||
|
spec.paramSpec = paramSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", nil, fmt.Errorf("unsupported header parameter type: %s", schema.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryParamStructParts(paramName string, queryParamStruct interface{}) []string {
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
part := paramName
|
||||||
|
|
||||||
|
switch v := queryParamStruct.(type) {
|
||||||
|
case string:
|
||||||
|
part += "=" + v
|
||||||
|
parts = append(parts, part)
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
for n, item := range v {
|
||||||
|
part := fmt.Sprintf("%s[%d]", part, n)
|
||||||
|
parts = append(parts, queryParamStructParts(part, item)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
case map[string]interface{}:
|
||||||
|
for k, v := range v {
|
||||||
|
part := fmt.Sprintf("%s[%s]", part, k)
|
||||||
|
|
||||||
|
parts = append(parts, queryParamStructParts(part, v)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts
|
||||||
|
}
|
||||||
27
AttackSurface/src/gotestwaf/internal/openapi/placeholders.go
Normal file
27
AttackSurface/src/gotestwaf/internal/openapi/placeholders.go
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package openapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/wallarm/gotestwaf/internal/payload/placeholder"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
headerPlaceholder string
|
||||||
|
urlParamPlaceholder string
|
||||||
|
urlPathPlaceholder string
|
||||||
|
htmlFormPlaceholder string
|
||||||
|
jsonBodyPlaceholder string
|
||||||
|
jsonRequestPlaceholder string
|
||||||
|
xmlBodyPlaceholder string
|
||||||
|
requestBodyPlaceholder string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
headerPlaceholder = placeholder.DefaultHeader.GetName()
|
||||||
|
urlParamPlaceholder = placeholder.DefaultURLParam.GetName()
|
||||||
|
urlPathPlaceholder = placeholder.DefaultURLPath.GetName()
|
||||||
|
jsonBodyPlaceholder = placeholder.DefaultJSONBody.GetName()
|
||||||
|
jsonRequestPlaceholder = placeholder.DefaultJSONRequest.GetName()
|
||||||
|
htmlFormPlaceholder = placeholder.DefaultHTMLForm.GetName()
|
||||||
|
xmlBodyPlaceholder = placeholder.DefaultXMLBody.GetName()
|
||||||
|
requestBodyPlaceholder = placeholder.DefaultRequestBody.GetName()
|
||||||
|
}
|
||||||
293
AttackSurface/src/gotestwaf/internal/openapi/schema.go
Normal file
293
AttackSurface/src/gotestwaf/internal/openapi/schema.go
Normal file
|
|
@ -0,0 +1,293 @@
|
||||||
|
package openapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/clbanning/mxj"
|
||||||
|
"github.com/getkin/kin-openapi/openapi3"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
jsonContentType = "application/json"
|
||||||
|
xmlContentType = "application/xml"
|
||||||
|
xWwwFormContentType = "application/x-www-form-urlencoded"
|
||||||
|
plainTextContentType = "text/plain"
|
||||||
|
|
||||||
|
xmlAttributePrefix = "-"
|
||||||
|
xmlHeader = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
|
||||||
|
)
|
||||||
|
|
||||||
|
// schemaToMap converts openapi3.Schema to value or map[string]interface{}.
|
||||||
|
func schemaToMap(name string, schema *openapi3.Schema, isXML bool) (
|
||||||
|
value interface{},
|
||||||
|
strAvailable bool,
|
||||||
|
paramSpec map[string]*parameterSpec,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
var allOf openapi3.SchemaRefs
|
||||||
|
if schema.AnyOf != nil {
|
||||||
|
allOf = schema.AnyOf
|
||||||
|
}
|
||||||
|
if schema.AllOf != nil {
|
||||||
|
allOf = schema.AllOf
|
||||||
|
}
|
||||||
|
if allOf != nil {
|
||||||
|
allOfValue := make(map[string]interface{})
|
||||||
|
allOfParamSpec := make(map[string]*parameterSpec)
|
||||||
|
allOfStrAvailable := false
|
||||||
|
|
||||||
|
for _, schemaRef := range allOf {
|
||||||
|
if schemaRef != nil && schemaRef.Value != nil {
|
||||||
|
innerSchema := schemaRef.Value
|
||||||
|
|
||||||
|
innerValue, innerStrAvailable, innerParamSpec, innerErr := schemaToMap(name, innerSchema, isXML)
|
||||||
|
if innerErr != nil {
|
||||||
|
return nil, false, nil, errors.Wrap(innerErr, "couldn't parse allOf/anyOf")
|
||||||
|
}
|
||||||
|
|
||||||
|
innerMap, ok := innerValue.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, false, nil, errors.New("unsupported object in allOf/anyOf")
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range innerMap {
|
||||||
|
allOfValue[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range innerParamSpec {
|
||||||
|
allOfParamSpec[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
allOfStrAvailable = allOfStrAvailable || innerStrAvailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allOfValue, allOfStrAvailable, allOfParamSpec, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.OneOf != nil {
|
||||||
|
for _, schemaRef := range schema.OneOf {
|
||||||
|
if schemaRef != nil && schemaRef.Value != nil {
|
||||||
|
innerSchema := schemaRef.Value
|
||||||
|
|
||||||
|
innerValue, innerStrAvailable, innerParamSpec, innerErr := schemaToMap(name, innerSchema, isXML)
|
||||||
|
if innerErr != nil {
|
||||||
|
return nil, false, nil, errors.Wrap(innerErr, "couldn't parse oneOf")
|
||||||
|
}
|
||||||
|
|
||||||
|
innerMap, ok := innerValue.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, false, nil, errors.New("unsupported object in oneOf")
|
||||||
|
}
|
||||||
|
|
||||||
|
if innerStrAvailable && len(innerParamSpec) > len(paramSpec) {
|
||||||
|
value = innerMap
|
||||||
|
paramSpec = innerParamSpec
|
||||||
|
strAvailable = innerStrAvailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
strAvailable = false
|
||||||
|
|
||||||
|
if isXML && schema.XML != nil {
|
||||||
|
if schema.XML.Name != "" {
|
||||||
|
name = schema.XML.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if name != "" {
|
||||||
|
if schema.XML.Attribute {
|
||||||
|
name = xmlAttributePrefix + name
|
||||||
|
} else if schema.XML.Prefix != "" {
|
||||||
|
name = schema.XML.Prefix + ":" + name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paramType := ""
|
||||||
|
if schema.Type != nil && len(*schema.Type) > 0 {
|
||||||
|
paramType = (*schema.Type)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch paramType {
|
||||||
|
case openapi3.TypeInteger:
|
||||||
|
randInt := genRandomInt(schema.Min, schema.Max, schema.ExclusiveMin, schema.ExclusiveMax)
|
||||||
|
value = fmt.Sprintf("%d", randInt)
|
||||||
|
|
||||||
|
case openapi3.TypeNumber:
|
||||||
|
randFloat := genRandomFloat(schema.Min, schema.Max, schema.ExclusiveMin, schema.ExclusiveMax)
|
||||||
|
value = fmt.Sprintf("%f", randFloat)
|
||||||
|
|
||||||
|
case openapi3.TypeString:
|
||||||
|
value = genRandomPlaceholder()
|
||||||
|
strAvailable = true
|
||||||
|
|
||||||
|
spec := ¶meterSpec{}
|
||||||
|
spec.paramType = paramType
|
||||||
|
spec.minLength = schema.MinLength
|
||||||
|
if schema.MaxLength == nil {
|
||||||
|
spec.maxLength = math.MaxUint64
|
||||||
|
spec.value = genRandomString(spec.minLength, spec.minLength+defaultStringSize)
|
||||||
|
} else {
|
||||||
|
spec.maxLength = *schema.MaxLength
|
||||||
|
spec.value = genRandomString(spec.minLength, spec.maxLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
paramSpec = make(map[string]*parameterSpec)
|
||||||
|
paramSpec[value.(string)] = spec
|
||||||
|
|
||||||
|
case openapi3.TypeBoolean:
|
||||||
|
value = "false"
|
||||||
|
|
||||||
|
case openapi3.TypeArray:
|
||||||
|
inner, innerStrAvailable, innerParamSpec, err := schemaToMap(name, schema.Items.Value, isXML)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, nil, errors.Wrap(err, "couldn't parse array")
|
||||||
|
}
|
||||||
|
|
||||||
|
minArrayLength := int(schema.MinLength)
|
||||||
|
if minArrayLength == 0 {
|
||||||
|
minArrayLength = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
v := make([]interface{}, minArrayLength)
|
||||||
|
for i := 0; i < minArrayLength; i++ {
|
||||||
|
v[i] = inner
|
||||||
|
}
|
||||||
|
|
||||||
|
value = v
|
||||||
|
strAvailable = innerStrAvailable
|
||||||
|
paramSpec = innerParamSpec
|
||||||
|
|
||||||
|
case openapi3.TypeObject:
|
||||||
|
paramSpec = make(map[string]*parameterSpec)
|
||||||
|
mapStructure := make(map[string]interface{})
|
||||||
|
|
||||||
|
for name, obj := range schema.Properties {
|
||||||
|
inner, innerStrAvailable, innerParamSpec, err := schemaToMap(name, obj.Value, isXML)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, nil, errors.Wrap(err, "couldn't parse object")
|
||||||
|
}
|
||||||
|
|
||||||
|
strAvailable = strAvailable || innerStrAvailable
|
||||||
|
|
||||||
|
innerMap, isInnerMap := inner.(map[string]interface{})
|
||||||
|
if isXML && isInnerMap {
|
||||||
|
for k, v := range innerMap {
|
||||||
|
mapStructure[k] = v
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mapStructure[name] = inner
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range innerParamSpec {
|
||||||
|
paramSpec[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = mapStructure
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, false, nil, fmt.Errorf("unknown schema type: %s", schema.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isXML {
|
||||||
|
var wrappedValue map[string]interface{}
|
||||||
|
|
||||||
|
if mapValue, ok := value.(map[string]interface{}); ok {
|
||||||
|
wrappedValue = mapValue
|
||||||
|
} else {
|
||||||
|
wrappedValue = make(map[string]interface{})
|
||||||
|
wrappedValue["#text"] = value
|
||||||
|
wrappedValue["#seq"] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.XML != nil && schema.XML.Namespace != "" {
|
||||||
|
xmlns := "xmlns"
|
||||||
|
if schema.XML.Prefix != "" {
|
||||||
|
xmlns = xmlns + ":" + schema.XML.Prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
wrappedValue["#attr"] = map[string]interface{}{
|
||||||
|
xmlns: map[string]interface{}{
|
||||||
|
"#text": schema.XML.Namespace,
|
||||||
|
"#seq": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = wrappedValue
|
||||||
|
|
||||||
|
if name != "" {
|
||||||
|
value = map[string]interface{}{
|
||||||
|
name: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, strAvailable, paramSpec, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// jsonMarshal dumps structure as JSON.
|
||||||
|
func jsonMarshal(schemaStructure interface{}) (string, error) {
|
||||||
|
byteString, err := json.Marshal(schemaStructure)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(byteString), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// xmlMarshal dumps structure as XML.
|
||||||
|
func xmlMarshal(schemaStructure interface{}) (string, error) {
|
||||||
|
object, ok := schemaStructure.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("input value must be map[string]interface{}")
|
||||||
|
}
|
||||||
|
|
||||||
|
m := mxj.Map(object)
|
||||||
|
|
||||||
|
byteString, err := m.XmlSeq()
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "couldn't marshall object to XML")
|
||||||
|
}
|
||||||
|
|
||||||
|
return xmlHeader + string(byteString), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// htmlFormMarshal dumps structure as HTML Form.
|
||||||
|
func htmlFormMarshal(schemaStructure interface{}) (string, error) {
|
||||||
|
object, ok := schemaStructure.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("input value must be map[string]interface{}")
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
var str string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for k, v := range object {
|
||||||
|
if strValue, isStr := v.(string); isStr {
|
||||||
|
str = strValue
|
||||||
|
} else {
|
||||||
|
str, err = jsonMarshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "couldn't marshall object to JSON in HTML form field")
|
||||||
|
}
|
||||||
|
str = url.QueryEscape(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
parts = append(parts, k+"="+str)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, "&"), nil
|
||||||
|
}
|
||||||
367
AttackSurface/src/gotestwaf/internal/openapi/template.go
Normal file
367
AttackSurface/src/gotestwaf/internal/openapi/template.go
Normal file
|
|
@ -0,0 +1,367 @@
|
||||||
|
package openapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/getkin/kin-openapi/openapi3"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Templates contains all templates generated from OpenAPI file. Templates are
|
||||||
|
// sorted by placeholders that can be used to substitute a malicious vector.
|
||||||
|
type Templates map[string][]*Template
|
||||||
|
|
||||||
|
// Template contains all information about template.
|
||||||
|
type Template struct {
|
||||||
|
Method string
|
||||||
|
Path string
|
||||||
|
URL string
|
||||||
|
|
||||||
|
PathParameters map[string]*parameterSpec
|
||||||
|
QueryParameters map[string]*parameterSpec
|
||||||
|
Headers map[string]*parameterSpec
|
||||||
|
RequestBodyParameters map[string]*parameterSpec
|
||||||
|
|
||||||
|
RequestBody map[string]string
|
||||||
|
|
||||||
|
Doc *openapi3.T
|
||||||
|
|
||||||
|
Placeholders map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTemplates parses OpenAPI document and returns all possible templates.
|
||||||
|
func NewTemplates(openapiDoc *openapi3.T, basePath string) (Templates, error) {
|
||||||
|
var unsortedTemplates []*Template
|
||||||
|
for path, info := range openapiDoc.Paths.Map() {
|
||||||
|
pathTemplates, err := pathTemplates(openapiDoc, basePath, path, info)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
unsortedTemplates = append(unsortedTemplates, pathTemplates...)
|
||||||
|
}
|
||||||
|
|
||||||
|
templates := make(Templates)
|
||||||
|
|
||||||
|
for _, template := range unsortedTemplates {
|
||||||
|
for placeholder, _ := range template.Placeholders {
|
||||||
|
if templates[placeholder] == nil {
|
||||||
|
templates[placeholder] = make([]*Template, 0)
|
||||||
|
}
|
||||||
|
templates[placeholder] = append(templates[placeholder], template)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathTemplates parses every path in OpenAPI document.
|
||||||
|
func pathTemplates(openapiDoc *openapi3.T, basePath string, path string, pathInfo *openapi3.PathItem) ([]*Template, error) {
|
||||||
|
var templates []*Template
|
||||||
|
|
||||||
|
if pathInfo.Connect != nil {
|
||||||
|
operationTemplate, err := operationTemplates(openapiDoc, basePath, path, http.MethodConnect, pathInfo.Connect)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
templates = append(templates, operationTemplate)
|
||||||
|
}
|
||||||
|
if pathInfo.Delete != nil {
|
||||||
|
operationTemplate, err := operationTemplates(openapiDoc, basePath, path, http.MethodDelete, pathInfo.Delete)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
templates = append(templates, operationTemplate)
|
||||||
|
}
|
||||||
|
if pathInfo.Get != nil {
|
||||||
|
operationTemplate, err := operationTemplates(openapiDoc, basePath, path, http.MethodGet, pathInfo.Get)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
templates = append(templates, operationTemplate)
|
||||||
|
}
|
||||||
|
if pathInfo.Head != nil {
|
||||||
|
operationTemplate, err := operationTemplates(openapiDoc, basePath, path, http.MethodGet, pathInfo.Get)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
templates = append(templates, operationTemplate)
|
||||||
|
}
|
||||||
|
if pathInfo.Options != nil {
|
||||||
|
operationTemplate, err := operationTemplates(openapiDoc, basePath, path, http.MethodOptions, pathInfo.Options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
templates = append(templates, operationTemplate)
|
||||||
|
}
|
||||||
|
if pathInfo.Patch != nil {
|
||||||
|
operationTemplate, err := operationTemplates(openapiDoc, basePath, path, http.MethodPatch, pathInfo.Patch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
templates = append(templates, operationTemplate)
|
||||||
|
}
|
||||||
|
if pathInfo.Post != nil {
|
||||||
|
operationTemplate, err := operationTemplates(openapiDoc, basePath, path, http.MethodPost, pathInfo.Post)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
templates = append(templates, operationTemplate)
|
||||||
|
}
|
||||||
|
if pathInfo.Put != nil {
|
||||||
|
operationTemplate, err := operationTemplates(openapiDoc, basePath, path, http.MethodPut, pathInfo.Put)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
templates = append(templates, operationTemplate)
|
||||||
|
}
|
||||||
|
if pathInfo.Trace != nil {
|
||||||
|
operationTemplate, err := operationTemplates(openapiDoc, basePath, path, http.MethodTrace, pathInfo.Trace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
templates = append(templates, operationTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// operationTemplates parses every operation in paths.
|
||||||
|
func operationTemplates(openapiDoc *openapi3.T, basePath string, path string, operationName string, operationInfo *openapi3.Operation) (*Template, error) {
|
||||||
|
template := &Template{
|
||||||
|
Method: operationName,
|
||||||
|
Path: path,
|
||||||
|
Doc: openapiDoc,
|
||||||
|
}
|
||||||
|
|
||||||
|
params, err := parseParameters(operationInfo.Parameters)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "couldn't parse request parameters")
|
||||||
|
}
|
||||||
|
|
||||||
|
template.PathParameters = params.pathParameters
|
||||||
|
template.QueryParameters = params.queryParameters
|
||||||
|
template.Headers = params.headers
|
||||||
|
template.URL = strings.TrimSuffix(basePath, "/") + path
|
||||||
|
|
||||||
|
placeholders := params.supportedPlaceholders
|
||||||
|
|
||||||
|
requestBody := make(map[string]string)
|
||||||
|
requestBodyParameters := make(map[string]*parameterSpec)
|
||||||
|
|
||||||
|
if operationInfo.RequestBody != nil {
|
||||||
|
for contentType, mediaType := range operationInfo.RequestBody.Value.Content {
|
||||||
|
rawBodyStruct, strAvailable, paramSpec, err := schemaToMap("", mediaType.Schema.Value, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "couldn't parse request body schema")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch contentType {
|
||||||
|
case jsonContentType:
|
||||||
|
if strAvailable {
|
||||||
|
placeholders[jsonRequestPlaceholder] = nil
|
||||||
|
placeholders[jsonBodyPlaceholder] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := jsonMarshal(rawBodyStruct)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody[contentType] = body
|
||||||
|
|
||||||
|
case xmlContentType:
|
||||||
|
rawBodyStruct, strAvailable, paramSpec, err = schemaToMap("", mediaType.Schema.Value, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "couldn't parse request body schema")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strAvailable {
|
||||||
|
placeholders[xmlBodyPlaceholder] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := xmlMarshal(rawBodyStruct)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody[contentType] = body
|
||||||
|
|
||||||
|
case xWwwFormContentType:
|
||||||
|
if strAvailable {
|
||||||
|
placeholders[htmlFormPlaceholder] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := htmlFormMarshal(rawBodyStruct)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody[contentType] = body
|
||||||
|
|
||||||
|
default:
|
||||||
|
if strAvailable {
|
||||||
|
placeholders[requestBodyPlaceholder] = nil
|
||||||
|
}
|
||||||
|
for k := range paramSpec {
|
||||||
|
requestBody[plainTextContentType] = k
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range paramSpec {
|
||||||
|
requestBodyParameters[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template.RequestBodyParameters = requestBodyParameters
|
||||||
|
template.RequestBody = requestBody
|
||||||
|
template.Placeholders = placeholders
|
||||||
|
|
||||||
|
return template, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRequest generates a new request with the payload substituted as
|
||||||
|
// the placeholder value.
|
||||||
|
func (t *Template) CreateRequest(ctx context.Context, placeholder string, payload string) (*http.Request, error) {
|
||||||
|
if _, ok := t.Placeholders[placeholder]; !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var body string
|
||||||
|
var contentType string
|
||||||
|
queryParams := make(map[string]string)
|
||||||
|
headers := make(map[string]string)
|
||||||
|
path := t.URL
|
||||||
|
|
||||||
|
switch placeholder {
|
||||||
|
case headerPlaceholder:
|
||||||
|
for header, spec := range t.Headers {
|
||||||
|
if spec.paramType == openapi3.TypeString {
|
||||||
|
payloadLen := uint64(len(payload))
|
||||||
|
if spec.minLength <= payloadLen && payloadLen <= spec.maxLength {
|
||||||
|
headers[header] = payload
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers[header] = spec.value
|
||||||
|
}
|
||||||
|
|
||||||
|
case urlPathPlaceholder:
|
||||||
|
for param, spec := range t.PathParameters {
|
||||||
|
if spec.paramType == openapi3.TypeString {
|
||||||
|
payloadLen := uint64(len(payload))
|
||||||
|
if spec.minLength <= payloadLen && payloadLen <= spec.maxLength {
|
||||||
|
path = strings.ReplaceAll(path, param, payload)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path = strings.ReplaceAll(path, param, spec.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
case urlParamPlaceholder:
|
||||||
|
for param, spec := range t.QueryParameters {
|
||||||
|
if spec.paramType == openapi3.TypeString {
|
||||||
|
payloadLen := uint64(len(payload))
|
||||||
|
if spec.minLength <= payloadLen && payloadLen <= spec.maxLength {
|
||||||
|
queryParams[param] = payload
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParams[param] = spec.value
|
||||||
|
}
|
||||||
|
|
||||||
|
case htmlFormPlaceholder:
|
||||||
|
body = t.RequestBody[xWwwFormContentType]
|
||||||
|
contentType = xWwwFormContentType
|
||||||
|
|
||||||
|
for paramDefaultValue, spec := range t.RequestBodyParameters {
|
||||||
|
if spec.paramType == openapi3.TypeString {
|
||||||
|
payloadLen := uint64(len(payload))
|
||||||
|
if spec.minLength <= payloadLen && payloadLen <= spec.maxLength {
|
||||||
|
body = strings.ReplaceAll(body, paramDefaultValue, payload)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case jsonBodyPlaceholder:
|
||||||
|
fallthrough
|
||||||
|
case jsonRequestPlaceholder:
|
||||||
|
body = t.RequestBody[jsonContentType]
|
||||||
|
contentType = jsonContentType
|
||||||
|
|
||||||
|
for paramDefaultValue, spec := range t.RequestBodyParameters {
|
||||||
|
if spec.paramType == openapi3.TypeString {
|
||||||
|
payloadLen := uint64(len(payload))
|
||||||
|
if spec.minLength <= payloadLen && payloadLen <= spec.maxLength {
|
||||||
|
body = strings.ReplaceAll(body, paramDefaultValue, payload)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case xmlBodyPlaceholder:
|
||||||
|
body = t.RequestBody[xmlContentType]
|
||||||
|
contentType = xmlContentType
|
||||||
|
|
||||||
|
for paramDefaultValue, spec := range t.RequestBodyParameters {
|
||||||
|
if spec.paramType == openapi3.TypeString {
|
||||||
|
payloadLen := uint64(len(payload))
|
||||||
|
if spec.minLength <= payloadLen && payloadLen <= spec.maxLength {
|
||||||
|
body = strings.ReplaceAll(body, paramDefaultValue, payload)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case requestBodyPlaceholder:
|
||||||
|
body = t.RequestBody[plainTextContentType]
|
||||||
|
contentType = plainTextContentType
|
||||||
|
|
||||||
|
for paramDefaultValue, spec := range t.RequestBodyParameters {
|
||||||
|
if spec.paramType == openapi3.TypeString {
|
||||||
|
payloadLen := uint64(len(payload))
|
||||||
|
if spec.minLength <= payloadLen && payloadLen <= spec.maxLength {
|
||||||
|
body = strings.ReplaceAll(body, paramDefaultValue, payload)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if placeholder != urlPathPlaceholder {
|
||||||
|
for k, v := range t.PathParameters {
|
||||||
|
path = strings.ReplaceAll(path, k, v.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, t.Method, path, bytes.NewReader([]byte(body)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "couldn't create request")
|
||||||
|
}
|
||||||
|
|
||||||
|
var params []string
|
||||||
|
for param, value := range queryParams {
|
||||||
|
params = append(params, param+"="+value)
|
||||||
|
}
|
||||||
|
req.URL.RawQuery = strings.Join(params, "&")
|
||||||
|
|
||||||
|
if contentType != "" {
|
||||||
|
req.Header.Add("Content-Type", contentType)
|
||||||
|
}
|
||||||
|
for header, value := range headers {
|
||||||
|
req.Header.Add(header, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package encoder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Base64EncoderNormalMode = iota
|
||||||
|
Base64EncoderFlatMode
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Encoder = (*Base64Encoder)(nil)
|
||||||
|
|
||||||
|
var DefaultBase64Encoder = &Base64Encoder{name: "Base64", mode: Base64EncoderNormalMode}
|
||||||
|
var DefaultBase64FlatEncoder = &Base64Encoder{name: "Base64Flat", mode: Base64EncoderFlatMode}
|
||||||
|
|
||||||
|
type Base64Encoder struct {
|
||||||
|
name string
|
||||||
|
mode uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Base64Encoder) GetName() string {
|
||||||
|
return enc.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Base64Encoder) Encode(data string) (string, error) {
|
||||||
|
switch enc.mode {
|
||||||
|
case Base64EncoderNormalMode:
|
||||||
|
res := base64.StdEncoding.EncodeToString([]byte(data))
|
||||||
|
return res, nil
|
||||||
|
case Base64EncoderFlatMode:
|
||||||
|
res := strings.ReplaceAll(base64.StdEncoding.EncodeToString([]byte(data)), "=", "")
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("undefined encoding method")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package encoder
|
||||||
|
|
||||||
|
type Encoder interface {
|
||||||
|
GetName() string
|
||||||
|
Encode(data string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var Encoders map[string]Encoder
|
||||||
|
|
||||||
|
var encoders = []Encoder{
|
||||||
|
DefaultBase64Encoder,
|
||||||
|
DefaultBase64FlatEncoder,
|
||||||
|
DefaultJSUnicodeEncoder,
|
||||||
|
DefaultPlainEncoder,
|
||||||
|
DefaultURLEncoder,
|
||||||
|
DefaultXMLEntityEncoder,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Encoders = make(map[string]Encoder)
|
||||||
|
for _, encoder := range encoders {
|
||||||
|
Encoders[encoder.GetName()] = encoder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Apply(encoderName, data string) (string, error) {
|
||||||
|
en, ok := Encoders[encoderName]
|
||||||
|
if !ok {
|
||||||
|
return "", &UnknownEncoderError{name: encoderName}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, err := en.Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package encoder
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
var _ error = (*UnknownEncoderError)(nil)
|
||||||
|
|
||||||
|
type UnknownEncoderError struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UnknownEncoderError) Error() string {
|
||||||
|
return fmt.Sprintf("unknown encoder: %s", e.name)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package encoder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/text/encoding/unicode"
|
||||||
|
"golang.org/x/text/transform"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Encoder = (*JSUnicodeEncoder)(nil)
|
||||||
|
|
||||||
|
var DefaultJSUnicodeEncoder = &JSUnicodeEncoder{name: "JSUnicode"}
|
||||||
|
|
||||||
|
type JSUnicodeEncoder struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *JSUnicodeEncoder) GetName() string {
|
||||||
|
return enc.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *JSUnicodeEncoder) Encode(data string) (string, error) {
|
||||||
|
encoder := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewEncoder()
|
||||||
|
utf16beStr, _, err := transform.Bytes(encoder, []byte(data))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := ""
|
||||||
|
for i := 0; i < len(utf16beStr); i += 2 {
|
||||||
|
ret += fmt.Sprintf("\\u%02x%02x", utf16beStr[i], utf16beStr[i+1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package encoder
|
||||||
|
|
||||||
|
var _ Encoder = (*PlainEncoder)(nil)
|
||||||
|
|
||||||
|
var DefaultPlainEncoder = &PlainEncoder{name: "Plain"}
|
||||||
|
|
||||||
|
type PlainEncoder struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *PlainEncoder) GetName() string {
|
||||||
|
return enc.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *PlainEncoder) Encode(data string) (string, error) {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
21
AttackSurface/src/gotestwaf/internal/payload/encoder/url.go
Normal file
21
AttackSurface/src/gotestwaf/internal/payload/encoder/url.go
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package encoder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Encoder = (*URLEncoder)(nil)
|
||||||
|
|
||||||
|
var DefaultURLEncoder = &URLEncoder{name: "URL"}
|
||||||
|
|
||||||
|
type URLEncoder struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *URLEncoder) GetName() string {
|
||||||
|
return enc.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *URLEncoder) Encode(data string) (string, error) {
|
||||||
|
return url.PathEscape(data), nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package encoder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Encoder = (*XMLEntityEncoder)(nil)
|
||||||
|
|
||||||
|
var DefaultXMLEntityEncoder = &XMLEntityEncoder{name: "XMLEntity"}
|
||||||
|
|
||||||
|
type XMLEntityEncoder struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *XMLEntityEncoder) GetName() string {
|
||||||
|
return enc.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *XMLEntityEncoder) Encode(data string) (string, error) {
|
||||||
|
b := bytes.NewBufferString("")
|
||||||
|
if err := xml.NewEncoder(b).Encode(data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return b.String(), nil
|
||||||
|
}
|
||||||
61
AttackSurface/src/gotestwaf/internal/payload/payload.go
Normal file
61
AttackSurface/src/gotestwaf/internal/payload/payload.go
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
package payload
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/payload/encoder"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/payload/placeholder"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PayloadInfo holds information about the payload and its configuration.
|
||||||
|
type PayloadInfo struct {
|
||||||
|
Payload string
|
||||||
|
EncoderName string
|
||||||
|
PlaceholderName string
|
||||||
|
PlaceholderConfig placeholder.PlaceholderConfig
|
||||||
|
|
||||||
|
DebugHeaderValue string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEncodedPayload encodes the payload using the specified encoder and
|
||||||
|
// returns the encoded payload.
|
||||||
|
func (p *PayloadInfo) GetEncodedPayload() (string, error) {
|
||||||
|
encodedPayload, err := encoder.Apply(p.EncoderName, p.Payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "couldn't encode payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
return encodedPayload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRequest generates a request based on the payload information, target URL,
|
||||||
|
// and client type.
|
||||||
|
func (p *PayloadInfo) GetRequest(targetURL string, clientType types.HTTPClientType) (types.Request, error) {
|
||||||
|
encodedPayload, err := p.GetEncodedPayload()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := placeholder.Apply(
|
||||||
|
targetURL,
|
||||||
|
encodedPayload,
|
||||||
|
p.PlaceholderName,
|
||||||
|
p.PlaceholderConfig,
|
||||||
|
clientType,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, fmt.Sprintf("couldn't apply placeholder %s", p.PlaceholderName))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r := request.(type) {
|
||||||
|
case *types.GoHTTPRequest:
|
||||||
|
r.DebugHeaderValue = p.DebugHeaderValue
|
||||||
|
case *types.ChromeDPTasks:
|
||||||
|
r.DebugHeaderValue = p.DebugHeaderValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return request, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
var _ error = (*UnknownPlaceholderError)(nil)
|
||||||
|
var _ error = (*BadPlaceholderConfigError)(nil)
|
||||||
|
|
||||||
|
type UnknownPlaceholderError struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UnknownPlaceholderError) Error() string {
|
||||||
|
return fmt.Sprintf("unknown placeholder: %s", e.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BadPlaceholderConfigError struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *BadPlaceholderConfigError) Error() string {
|
||||||
|
if e.err != nil {
|
||||||
|
return fmt.Sprintf("bad config for %s placeholder: %s", e.name, e.err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("bad config for %s placeholder", e.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *BadPlaceholderConfigError) Unwrap() error {
|
||||||
|
return e.err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GraphQL struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GraphQLConfig struct {
|
||||||
|
Method string
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultGraphQL = &GraphQL{name: "GraphQL"}
|
||||||
|
|
||||||
|
var _ Placeholder = (*GraphQL)(nil)
|
||||||
|
|
||||||
|
func (p *GraphQL) NewPlaceholderConfig(conf map[any]any) (PlaceholderConfig, error) {
|
||||||
|
result := &GraphQLConfig{}
|
||||||
|
|
||||||
|
method, ok := conf["method"]
|
||||||
|
if !ok {
|
||||||
|
return nil, &BadPlaceholderConfigError{
|
||||||
|
name: p.name,
|
||||||
|
err: errors.New("empty method"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.Method, ok = method.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, &BadPlaceholderConfigError{
|
||||||
|
name: p.name,
|
||||||
|
err: errors.Errorf("unknown type of 'method' field, expected string, got %T", method),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch result.Method {
|
||||||
|
case http.MethodGet, http.MethodPost:
|
||||||
|
return result, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, &BadPlaceholderConfigError{
|
||||||
|
name: p.name,
|
||||||
|
err: errors.Errorf("unknown HTTP method, expected GET or POST, got %T", result.Method),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GraphQL) GetName() string {
|
||||||
|
return p.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GraphQL) CreateRequest(requestURL, payload string, config PlaceholderConfig, httpClientType types.HTTPClientType) (types.Request, error) {
|
||||||
|
if httpClientType != types.GoHTTPClient {
|
||||||
|
return nil, errors.New("CreateRequest only support GoHTTPClient")
|
||||||
|
}
|
||||||
|
|
||||||
|
conf, ok := config.(*GraphQLConfig)
|
||||||
|
if !ok {
|
||||||
|
return nil, &BadPlaceholderConfigError{
|
||||||
|
name: p.name,
|
||||||
|
err: errors.Errorf("bad config type: got %T, expected: %T", config, &GraphQLConfig{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reqURL, err := url.Parse(requestURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqest := &types.GoHTTPRequest{}
|
||||||
|
|
||||||
|
switch conf.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
queryParams := reqURL.Query()
|
||||||
|
queryParams.Set("query", payload)
|
||||||
|
reqURL.RawQuery = queryParams.Encode()
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, reqURL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqest.Req = req
|
||||||
|
|
||||||
|
return reqest, nil
|
||||||
|
|
||||||
|
case http.MethodPost:
|
||||||
|
req, err := http.NewRequest(http.MethodPost, reqURL.String(), strings.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqest.Req = req
|
||||||
|
|
||||||
|
return reqest, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, &BadPlaceholderConfigError{
|
||||||
|
name: p.name,
|
||||||
|
err: errors.Errorf("unknown HTTP method, expected GET or POST, got %T", conf.Method),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GraphQLConfig) Hash() []byte {
|
||||||
|
sha256sum := sha256.New()
|
||||||
|
sha256sum.Write([]byte(g.Method))
|
||||||
|
return sha256sum.Sum(nil)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Placeholder = (*GRPC)(nil)
|
||||||
|
|
||||||
|
var DefaultGRPC = &GRPC{name: "gRPC"}
|
||||||
|
|
||||||
|
type GRPC struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GRPC) NewPlaceholderConfig(map[any]any) (PlaceholderConfig, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GRPC) GetName() string {
|
||||||
|
return p.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GRPC) CreateRequest(string, string, PlaceholderConfig, types.HTTPClientType) (types.Request, error) {
|
||||||
|
return nil, errors.New("not implemented")
|
||||||
|
}
|
||||||
212
AttackSurface/src/gotestwaf/internal/payload/placeholder/grpc/service.pb.go
generated
Normal file
212
AttackSurface/src/gotestwaf/internal/payload/placeholder/grpc/service.pb.go
generated
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.33.0
|
||||||
|
// protoc v4.25.3
|
||||||
|
// source: service.proto
|
||||||
|
|
||||||
|
package encoder
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Request) Reset() {
|
||||||
|
*x = Request{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_service_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Request) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Request) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Request) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_service_proto_msgTypes[0]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use Request.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Request) Descriptor() ([]byte, []int) {
|
||||||
|
return file_service_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Request) GetValue() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Value
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Response) Reset() {
|
||||||
|
*x = Response{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_service_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Response) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Response) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Response) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_service_proto_msgTypes[1]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use Response.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Response) Descriptor() ([]byte, []int) {
|
||||||
|
return file_service_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Response) GetValue() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Value
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_service_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
var file_service_proto_rawDesc = []byte{
|
||||||
|
0x0a, 0x0d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
|
||||||
|
0x07, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x22, 0x1f, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75,
|
||||||
|
0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01,
|
||||||
|
0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x20, 0x0a, 0x08, 0x52, 0x65, 0x73,
|
||||||
|
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01,
|
||||||
|
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0x3b, 0x0a, 0x0d, 0x53,
|
||||||
|
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x46, 0x6f, 0x6f, 0x42, 0x61, 0x72, 0x12, 0x2a, 0x0a, 0x03,
|
||||||
|
0x66, 0x6f, 0x6f, 0x12, 0x10, 0x2e, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x52, 0x65,
|
||||||
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e,
|
||||||
|
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68,
|
||||||
|
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x77, 0x61, 0x6c, 0x6c, 0x61, 0x72, 0x6d, 0x2f, 0x67,
|
||||||
|
0x6f, 0x74, 0x65, 0x73, 0x74, 0x77, 0x61, 0x66, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
|
||||||
|
0x6c, 0x2f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x2f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x65,
|
||||||
|
0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_service_proto_rawDescOnce sync.Once
|
||||||
|
file_service_proto_rawDescData = file_service_proto_rawDesc
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_service_proto_rawDescGZIP() []byte {
|
||||||
|
file_service_proto_rawDescOnce.Do(func() {
|
||||||
|
file_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_service_proto_rawDescData)
|
||||||
|
})
|
||||||
|
return file_service_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_service_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||||
|
var file_service_proto_goTypes = []interface{}{
|
||||||
|
(*Request)(nil), // 0: encoder.Request
|
||||||
|
(*Response)(nil), // 1: encoder.Response
|
||||||
|
}
|
||||||
|
var file_service_proto_depIdxs = []int32{
|
||||||
|
0, // 0: encoder.ServiceFooBar.foo:input_type -> encoder.Request
|
||||||
|
1, // 1: encoder.ServiceFooBar.foo:output_type -> encoder.Response
|
||||||
|
1, // [1:2] is the sub-list for method output_type
|
||||||
|
0, // [0:1] is the sub-list for method input_type
|
||||||
|
0, // [0:0] is the sub-list for extension type_name
|
||||||
|
0, // [0:0] is the sub-list for extension extendee
|
||||||
|
0, // [0:0] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_service_proto_init() }
|
||||||
|
func file_service_proto_init() {
|
||||||
|
if File_service_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !protoimpl.UnsafeEnabled {
|
||||||
|
file_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*Request); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*Response); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: file_service_proto_rawDesc,
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 2,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 1,
|
||||||
|
},
|
||||||
|
GoTypes: file_service_proto_goTypes,
|
||||||
|
DependencyIndexes: file_service_proto_depIdxs,
|
||||||
|
MessageInfos: file_service_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_service_proto = out.File
|
||||||
|
file_service_proto_rawDesc = nil
|
||||||
|
file_service_proto_goTypes = nil
|
||||||
|
file_service_proto_depIdxs = nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
package encoder;
|
||||||
|
option go_package = "github.com/wallarm/gotestwaf/internal/payload/encoder";
|
||||||
|
|
||||||
|
message Request {
|
||||||
|
string value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Response {
|
||||||
|
string value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
service ServiceFooBar {
|
||||||
|
rpc foo(Request) returns (Response);
|
||||||
|
}
|
||||||
102
AttackSurface/src/gotestwaf/internal/payload/placeholder/grpc/service_grpc.pb.go
generated
Normal file
102
AttackSurface/src/gotestwaf/internal/payload/placeholder/grpc/service_grpc.pb.go
generated
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
|
||||||
|
package encoder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.32.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion7
|
||||||
|
|
||||||
|
// ServiceFooBarClient is the client API for ServiceFooBar service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
type ServiceFooBarClient interface {
|
||||||
|
Foo(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type serviceFooBarClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServiceFooBarClient(cc grpc.ClientConnInterface) ServiceFooBarClient {
|
||||||
|
return &serviceFooBarClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *serviceFooBarClient) Foo(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) {
|
||||||
|
out := new(Response)
|
||||||
|
err := c.cc.Invoke(ctx, "/encoder.ServiceFooBar/foo", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceFooBarServer is the server API for ServiceFooBar service.
|
||||||
|
// All implementations must embed UnimplementedServiceFooBarServer
|
||||||
|
// for forward compatibility
|
||||||
|
type ServiceFooBarServer interface {
|
||||||
|
Foo(context.Context, *Request) (*Response, error)
|
||||||
|
mustEmbedUnimplementedServiceFooBarServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedServiceFooBarServer must be embedded to have forward compatible implementations.
|
||||||
|
type UnimplementedServiceFooBarServer struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedServiceFooBarServer) Foo(context.Context, *Request) (*Response, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method Foo not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedServiceFooBarServer) mustEmbedUnimplementedServiceFooBarServer() {}
|
||||||
|
|
||||||
|
// UnsafeServiceFooBarServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to ServiceFooBarServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeServiceFooBarServer interface {
|
||||||
|
mustEmbedUnimplementedServiceFooBarServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterServiceFooBarServer(s grpc.ServiceRegistrar, srv ServiceFooBarServer) {
|
||||||
|
s.RegisterService(&ServiceFooBar_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _ServiceFooBar_Foo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(Request)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ServiceFooBarServer).Foo(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/encoder.ServiceFooBar/foo",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ServiceFooBarServer).Foo(ctx, req.(*Request))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceFooBar_ServiceDesc is the grpc.ServiceDesc for ServiceFooBar service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var ServiceFooBar_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "encoder.ServiceFooBar",
|
||||||
|
HandlerType: (*ServiceFooBarServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "foo",
|
||||||
|
Handler: _ServiceFooBar_Foo_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "service.proto",
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/clients/chrome/helpers"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Placeholder = (*Header)(nil)
|
||||||
|
|
||||||
|
var DefaultHeader = &Header{name: "Header"}
|
||||||
|
|
||||||
|
type Header struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Header) NewPlaceholderConfig(map[any]any) (PlaceholderConfig, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Header) GetName() string {
|
||||||
|
return p.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Header) CreateRequest(requestURL, payload string, config PlaceholderConfig, httpClientType types.HTTPClientType) (types.Request, error) {
|
||||||
|
reqURL, err := url.Parse(requestURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch httpClientType {
|
||||||
|
case types.GoHTTPClient:
|
||||||
|
return p.prepareGoHTTPClientRequest(reqURL.String(), payload, config)
|
||||||
|
case types.ChromeHTTPClient:
|
||||||
|
return p.prepareChromeHTTPClientRequest(reqURL.String(), payload, config)
|
||||||
|
default:
|
||||||
|
return nil, types.NewUnknownHTTPClientError(httpClientType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Header) prepareGoHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.GoHTTPRequest, error) {
|
||||||
|
randomName, err := RandomHex(Seed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
randomHeader := "X-" + randomName
|
||||||
|
req, err := http.NewRequest(http.MethodGet, requestURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add(randomHeader, payload)
|
||||||
|
|
||||||
|
return &types.GoHTTPRequest{Req: req}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Header) prepareChromeHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.ChromeDPTasks, error) {
|
||||||
|
randomName, err := RandomHex(Seed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
randomHeader := "X-" + randomName
|
||||||
|
|
||||||
|
reqOptions := &helpers.RequestOptions{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Headers: map[string]string{
|
||||||
|
randomHeader: payload,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
task, responseMeta, err := helpers.GetFetchRequest(requestURL, reqOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := &types.ChromeDPTasks{
|
||||||
|
Tasks: chromedp.Tasks{task},
|
||||||
|
ResponseMeta: responseMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/clients/chrome/helpers"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Placeholder = (*HTMLForm)(nil)
|
||||||
|
|
||||||
|
var DefaultHTMLForm = &HTMLForm{name: "HTMLForm"}
|
||||||
|
|
||||||
|
type HTMLForm struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HTMLForm) NewPlaceholderConfig(map[any]any) (PlaceholderConfig, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HTMLForm) GetName() string {
|
||||||
|
return p.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HTMLForm) CreateRequest(requestURL, payload string, config PlaceholderConfig, httpClientType types.HTTPClientType) (types.Request, error) {
|
||||||
|
reqURL, err := url.Parse(requestURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
randomName, err := RandomHex(Seed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyPayload := randomName + "=" + payload
|
||||||
|
|
||||||
|
switch httpClientType {
|
||||||
|
case types.GoHTTPClient:
|
||||||
|
return p.prepareGoHTTPClientRequest(reqURL.String(), bodyPayload, config)
|
||||||
|
case types.ChromeHTTPClient:
|
||||||
|
return p.prepareChromeHTTPClientRequest(reqURL.String(), bodyPayload, config)
|
||||||
|
default:
|
||||||
|
return nil, types.NewUnknownHTTPClientError(httpClientType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HTMLForm) prepareGoHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.GoHTTPRequest, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodPost, requestURL, strings.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
return &types.GoHTTPRequest{Req: req}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HTMLForm) prepareChromeHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.ChromeDPTasks, error) {
|
||||||
|
reqOptions := &helpers.RequestOptions{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
Body: fmt.Sprintf(`"%s"`, template.JSEscaper(payload)),
|
||||||
|
}
|
||||||
|
|
||||||
|
task, responseMeta, err := helpers.GetFetchRequest(requestURL, reqOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := &types.ChromeDPTasks{
|
||||||
|
Tasks: chromedp.Tasks{task},
|
||||||
|
ResponseMeta: responseMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/clients/chrome/helpers"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Placeholder = (*HTMLMultipartForm)(nil)
|
||||||
|
|
||||||
|
var DefaultHTMLMultipartForm = &HTMLMultipartForm{name: "HTMLMultipartForm"}
|
||||||
|
|
||||||
|
type HTMLMultipartForm struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HTMLMultipartForm) NewPlaceholderConfig(map[any]any) (PlaceholderConfig, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HTMLMultipartForm) GetName() string {
|
||||||
|
return p.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HTMLMultipartForm) CreateRequest(requestURL, payload string, config PlaceholderConfig, httpClientType types.HTTPClientType) (types.Request, error) {
|
||||||
|
reqURL, err := url.Parse(requestURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch httpClientType {
|
||||||
|
case types.GoHTTPClient:
|
||||||
|
return p.prepareGoHTTPClientRequest(reqURL.String(), payload, config)
|
||||||
|
case types.ChromeHTTPClient:
|
||||||
|
return p.prepareChromeHTTPClientRequest(reqURL.String(), payload, config)
|
||||||
|
default:
|
||||||
|
return nil, types.NewUnknownHTTPClientError(httpClientType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HTMLMultipartForm) prepareGoHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.GoHTTPRequest, error) {
|
||||||
|
randomName, err := RandomHex(Seed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
|
||||||
|
fw, err := writer.CreateFormField(randomName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fw.Write([]byte(payload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Close()
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewReader(body.Bytes()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
|
return &types.GoHTTPRequest{Req: req}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HTMLMultipartForm) prepareChromeHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.ChromeDPTasks, error) {
|
||||||
|
randomName, err := RandomHex(Seed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqOptions := &helpers.RequestOptions{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Body: fmt.Sprintf(
|
||||||
|
`(() => { const formData = new FormData(); formData.append("%s", "%s"); return formData; })()`,
|
||||||
|
randomName, template.JSEscaper(payload),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
task, responseMeta, err := helpers.GetFetchRequest(requestURL, reqOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := &types.ChromeDPTasks{
|
||||||
|
Tasks: chromedp.Tasks{task},
|
||||||
|
ResponseMeta: responseMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/clients/chrome/helpers"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Placeholder = (*JSONBody)(nil)
|
||||||
|
|
||||||
|
var DefaultJSONBody = &JSONBody{name: "JSONBody"}
|
||||||
|
|
||||||
|
type JSONBody struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSONBody) NewPlaceholderConfig(map[any]any) (PlaceholderConfig, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSONBody) GetName() string {
|
||||||
|
return p.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSONBody) CreateRequest(requestURL, payload string, config PlaceholderConfig, httpClientType types.HTTPClientType) (types.Request, error) {
|
||||||
|
reqURL, err := url.Parse(requestURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch httpClientType {
|
||||||
|
case types.GoHTTPClient:
|
||||||
|
return p.prepareGoHTTPClientRequest(reqURL.String(), payload, config)
|
||||||
|
case types.ChromeHTTPClient:
|
||||||
|
return p.prepareChromeHTTPClientRequest(reqURL.String(), payload, config)
|
||||||
|
default:
|
||||||
|
return nil, types.NewUnknownHTTPClientError(httpClientType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSONBody) prepareGoHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.GoHTTPRequest, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodPost, requestURL, strings.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
return &types.GoHTTPRequest{Req: req}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSONBody) prepareChromeHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.ChromeDPTasks, error) {
|
||||||
|
reqOptions := &helpers.RequestOptions{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
Body: fmt.Sprintf(`"%s"`, template.JSEscaper(payload)),
|
||||||
|
}
|
||||||
|
|
||||||
|
task, responseMeta, err := helpers.GetFetchRequest(requestURL, reqOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := &types.ChromeDPTasks{
|
||||||
|
Tasks: chromedp.Tasks{task},
|
||||||
|
ResponseMeta: responseMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/clients/chrome/helpers"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/payload/encoder"
|
||||||
|
)
|
||||||
|
|
||||||
|
const jsonRequestPayloadWrapper = `{"test": true, "%s": "%s"}`
|
||||||
|
|
||||||
|
var _ Placeholder = (*JSONRequest)(nil)
|
||||||
|
|
||||||
|
var DefaultJSONRequest = &JSONRequest{name: "JSONRequest"}
|
||||||
|
|
||||||
|
type JSONRequest struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSONRequest) NewPlaceholderConfig(map[any]any) (PlaceholderConfig, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSONRequest) GetName() string {
|
||||||
|
return p.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSONRequest) CreateRequest(requestURL, payload string, config PlaceholderConfig, httpClientType types.HTTPClientType) (types.Request, error) {
|
||||||
|
reqURL, err := url.Parse(requestURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
param, err := RandomHex(Seed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedPayload, err := encoder.Apply("JSUnicode", payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonPayload := fmt.Sprintf(jsonRequestPayloadWrapper, param, encodedPayload)
|
||||||
|
|
||||||
|
switch httpClientType {
|
||||||
|
case types.GoHTTPClient:
|
||||||
|
return p.prepareGoHTTPClientRequest(reqURL.String(), jsonPayload, config)
|
||||||
|
case types.ChromeHTTPClient:
|
||||||
|
return p.prepareChromeHTTPClientRequest(reqURL.String(), jsonPayload, config)
|
||||||
|
default:
|
||||||
|
return nil, types.NewUnknownHTTPClientError(httpClientType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSONRequest) prepareGoHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.GoHTTPRequest, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodPost, requestURL, strings.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
return &types.GoHTTPRequest{Req: req}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSONRequest) prepareChromeHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.ChromeDPTasks, error) {
|
||||||
|
reqOptions := &helpers.RequestOptions{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
Body: fmt.Sprintf(`"%s"`, template.JSEscaper(payload)),
|
||||||
|
}
|
||||||
|
|
||||||
|
task, responseMeta, err := helpers.GetFetchRequest(requestURL, reqOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := &types.ChromeDPTasks{
|
||||||
|
Tasks: chromedp.Tasks{task},
|
||||||
|
ResponseMeta: responseMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/helpers"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Seed = 5
|
||||||
|
|
||||||
|
payloadPlaceholder = "{{payload}}"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Placeholder interface {
|
||||||
|
NewPlaceholderConfig(conf map[any]any) (PlaceholderConfig, error)
|
||||||
|
|
||||||
|
GetName() string
|
||||||
|
CreateRequest(
|
||||||
|
requestURL, payload string,
|
||||||
|
config PlaceholderConfig,
|
||||||
|
httpClientType types.HTTPClientType,
|
||||||
|
) (types.Request, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlaceholderConfig interface {
|
||||||
|
helpers.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
var Placeholders map[string]Placeholder
|
||||||
|
|
||||||
|
var placeholders = []Placeholder{
|
||||||
|
DefaultGraphQL,
|
||||||
|
DefaultGRPC,
|
||||||
|
DefaultHeader,
|
||||||
|
DefaultHTMLForm,
|
||||||
|
DefaultHTMLMultipartForm,
|
||||||
|
DefaultJSONBody,
|
||||||
|
DefaultJSONRequest,
|
||||||
|
DefaultRawRequest,
|
||||||
|
DefaultRequestBody,
|
||||||
|
DefaultSOAPBody,
|
||||||
|
DefaultURLParam,
|
||||||
|
DefaultURLPath,
|
||||||
|
DefaultUserAgent,
|
||||||
|
DefaultXMLBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Placeholders = make(map[string]Placeholder)
|
||||||
|
for _, placeholder := range placeholders {
|
||||||
|
Placeholders[placeholder.GetName()] = placeholder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPlaceholderConfig(name string, conf any) (PlaceholderConfig, error) {
|
||||||
|
ph, ok := Placeholders[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, &UnknownPlaceholderError{name: name}
|
||||||
|
}
|
||||||
|
|
||||||
|
phConfMap, ok := conf.(map[any]any)
|
||||||
|
if !ok {
|
||||||
|
return nil, &BadPlaceholderConfigError{
|
||||||
|
name: name,
|
||||||
|
err: errors.Errorf("bad placeholder config, expected: map[any]any, got: %T", conf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
phConf, err := ph.NewPlaceholderConfig(phConfMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &BadPlaceholderConfigError{
|
||||||
|
name: name,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return phConf, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func Apply(
|
||||||
|
url, data, placeholder string,
|
||||||
|
config PlaceholderConfig,
|
||||||
|
httpClientType types.HTTPClientType,
|
||||||
|
) (types.Request, error) {
|
||||||
|
ph, ok := Placeholders[placeholder]
|
||||||
|
if !ok {
|
||||||
|
return nil, &UnknownPlaceholderError{name: placeholder}
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := ph.CreateRequest(url, data, config, httpClientType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,207 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/clients/chrome/helpers"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Placeholder = (*RawRequest)(nil)
|
||||||
|
var _ PlaceholderConfig = (*RawRequestConfig)(nil)
|
||||||
|
|
||||||
|
var DefaultRawRequest = &RawRequest{name: "RawRequest"}
|
||||||
|
|
||||||
|
type RawRequest struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RawRequestConfig struct {
|
||||||
|
Method string
|
||||||
|
Path string
|
||||||
|
Headers map[string]string
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RawRequest) NewPlaceholderConfig(conf map[any]any) (PlaceholderConfig, error) {
|
||||||
|
result := &RawRequestConfig{}
|
||||||
|
|
||||||
|
method, ok := conf["method"]
|
||||||
|
if !ok {
|
||||||
|
return nil, &BadPlaceholderConfigError{
|
||||||
|
name: p.name,
|
||||||
|
err: errors.New("empty method"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.Method, ok = method.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, &BadPlaceholderConfigError{
|
||||||
|
name: p.name,
|
||||||
|
err: errors.Errorf("unknown type of 'method' field, expected string, got %T", method),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path, ok := conf["path"]
|
||||||
|
if !ok {
|
||||||
|
result.Path = "/"
|
||||||
|
} else {
|
||||||
|
result.Path, ok = path.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, &BadPlaceholderConfigError{
|
||||||
|
name: p.name,
|
||||||
|
err: errors.Errorf("unknown type of 'path' field, expected string, got %T", path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Path) == 0 {
|
||||||
|
result.Path = "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Headers = make(map[string]string)
|
||||||
|
headers, ok := conf["headers"]
|
||||||
|
if ok {
|
||||||
|
typedHeaders, ok := headers.(map[any]any)
|
||||||
|
if !ok {
|
||||||
|
return nil, &BadPlaceholderConfigError{
|
||||||
|
name: p.name,
|
||||||
|
err: errors.Errorf("unknown type of 'headers' field, expected map[string]string, got %T", typedHeaders),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range typedHeaders {
|
||||||
|
header, okHeader := k.(string)
|
||||||
|
value, okValue := v.(string)
|
||||||
|
|
||||||
|
if !okHeader || !okValue {
|
||||||
|
return nil, &BadPlaceholderConfigError{
|
||||||
|
name: p.name,
|
||||||
|
err: errors.Errorf("unknown type of 'headers' field, expected map[string]string, got map[%T]%T", k, v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Headers[header] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body, ok := conf["body"]
|
||||||
|
if ok {
|
||||||
|
result.Body, ok = body.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, &BadPlaceholderConfigError{
|
||||||
|
name: p.name,
|
||||||
|
err: errors.Errorf("unknown type of 'body' field, expected string, got %T", body),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RawRequest) GetName() string {
|
||||||
|
return p.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRequest creates a new request from config.
|
||||||
|
// config must be a RawRequestConfig struct.
|
||||||
|
func (p *RawRequest) CreateRequest(requestURL, payload string, config PlaceholderConfig, httpClientType types.HTTPClientType) (types.Request, error) {
|
||||||
|
conf, ok := config.(*RawRequestConfig)
|
||||||
|
if !ok {
|
||||||
|
return nil, &BadPlaceholderConfigError{
|
||||||
|
name: p.name,
|
||||||
|
err: errors.Errorf("bad config type: got %T, expected: %T", config, &RawRequestConfig{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(requestURL, "/") {
|
||||||
|
requestURL += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(conf.Path, "/") {
|
||||||
|
requestURL += conf.Path[1:]
|
||||||
|
} else {
|
||||||
|
requestURL += conf.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
requestURL = strings.ReplaceAll(requestURL, payloadPlaceholder, url.PathEscape(payload))
|
||||||
|
|
||||||
|
switch httpClientType {
|
||||||
|
case types.GoHTTPClient:
|
||||||
|
return p.prepareGoHTTPClientRequest(requestURL, payload, conf)
|
||||||
|
case types.ChromeHTTPClient:
|
||||||
|
return p.prepareChromeHTTPClientRequest(requestURL, payload, conf)
|
||||||
|
default:
|
||||||
|
return nil, types.NewUnknownHTTPClientError(httpClientType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RawRequest) prepareGoHTTPClientRequest(requestURL, payload string, config *RawRequestConfig) (*types.GoHTTPRequest, error) {
|
||||||
|
var bodyReader io.Reader
|
||||||
|
body := strings.ReplaceAll(config.Body, payloadPlaceholder, payload)
|
||||||
|
if len(body) != 0 {
|
||||||
|
bodyReader = strings.NewReader(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(config.Method, requestURL, bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for k, v := range config.Headers {
|
||||||
|
req.Header.Add(k, strings.ReplaceAll(v, payloadPlaceholder, payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.GoHTTPRequest{Req: req}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RawRequest) prepareChromeHTTPClientRequest(requestURL, payload string, config *RawRequestConfig) (*types.ChromeDPTasks, error) {
|
||||||
|
headers := make(map[string]string)
|
||||||
|
for k, v := range config.Headers {
|
||||||
|
headers[k] = strings.ReplaceAll(v, payloadPlaceholder, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := fmt.Sprintf(`"%s"`, template.JSEscaper(strings.ReplaceAll(config.Body, payloadPlaceholder, payload)))
|
||||||
|
|
||||||
|
reqOptions := &helpers.RequestOptions{
|
||||||
|
Method: config.Method,
|
||||||
|
Headers: headers,
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
|
||||||
|
task, responseMeta, err := helpers.GetFetchRequest(requestURL, reqOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := &types.ChromeDPTasks{
|
||||||
|
Tasks: chromedp.Tasks{task},
|
||||||
|
ResponseMeta: responseMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RawRequestConfig) Hash() []byte {
|
||||||
|
sha256sum := sha256.New()
|
||||||
|
|
||||||
|
sha256sum.Write([]byte(r.Method))
|
||||||
|
sha256sum.Write([]byte(r.Path))
|
||||||
|
|
||||||
|
for header, value := range r.Headers {
|
||||||
|
sha256sum.Write([]byte(header))
|
||||||
|
sha256sum.Write([]byte(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
sha256sum.Write([]byte(r.Body))
|
||||||
|
|
||||||
|
return sha256sum.Sum(nil)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRawRequestNewConfig(t *testing.T) {
|
||||||
|
type checkFunc func(c any, e error)
|
||||||
|
|
||||||
|
var conf RawRequestConfig
|
||||||
|
|
||||||
|
getRef := func(s string) *string {
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
|
checkErr := func(c any, e error) {
|
||||||
|
if c != nil {
|
||||||
|
t.Error("config (*RawRequestConfig) must be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
var err *BadPlaceholderConfigError
|
||||||
|
if !errors.As(e, &err) {
|
||||||
|
t.Errorf("e should be an %T, got %T", err, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkValue := func(field any, value any) checkFunc {
|
||||||
|
return func(c any, e error) {
|
||||||
|
typedConf, ok := c.(*RawRequestConfig)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("bad conf type, got %v, expected %s", c, &conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e != nil {
|
||||||
|
t.Error("e (error) must be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
conf = *typedConf
|
||||||
|
if !reflect.DeepEqual(field, value) {
|
||||||
|
t.Errorf("bad value, got %v, expected %v", field, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
conf map[any]any
|
||||||
|
checkValue checkFunc
|
||||||
|
}{
|
||||||
|
{conf: map[any]any{}, checkValue: checkErr},
|
||||||
|
{conf: map[any]any{"method": 0}, checkValue: checkErr},
|
||||||
|
{conf: map[any]any{"method": http.MethodPost, "path": 0}, checkValue: checkErr},
|
||||||
|
{conf: map[any]any{"method": http.MethodPost, "path": "/", "headers": 0}, checkValue: checkErr},
|
||||||
|
{conf: map[any]any{"method": http.MethodPost, "path": "/", "headers": map[any]any{0: 0}}, checkValue: checkErr},
|
||||||
|
{conf: map[any]any{"method": http.MethodPost, "path": "/", "headers": map[any]any{"X-Test": 0}}, checkValue: checkErr},
|
||||||
|
{conf: map[any]any{"method": http.MethodPost, "path": "/"}, checkValue: checkValue(&conf.Method, getRef(http.MethodPost))},
|
||||||
|
{conf: map[any]any{"method": "abcd", "path": "/"}, checkValue: checkValue(&conf.Method, getRef("abcd"))},
|
||||||
|
{conf: map[any]any{"method": http.MethodPost}, checkValue: checkValue(&conf.Path, getRef("/"))},
|
||||||
|
{conf: map[any]any{"method": http.MethodPost, "path": "/abcd/{{payload}}"}, checkValue: checkValue(&conf.Path, getRef("/abcd/{{payload}}"))},
|
||||||
|
{conf: map[any]any{"method": http.MethodPost, "headers": map[any]any{"X-Test": "Test Header {{payload}}"}}, checkValue: checkValue(&conf.Headers, &map[string]string{"X-Test": "Test Header {{payload}}"})},
|
||||||
|
{conf: map[any]any{"method": http.MethodPost, "body": "Test {{payload}}"}, checkValue: checkValue(&conf.Body, getRef("Test {{payload}}"))},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
conf, err := DefaultRawRequest.NewPlaceholderConfig(test.conf)
|
||||||
|
test.checkValue(conf, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRawRequest(t *testing.T) {
|
||||||
|
type checkFunc func(r *http.Request)
|
||||||
|
|
||||||
|
const (
|
||||||
|
url = "http://example.com/"
|
||||||
|
testPayload = "0123456789abcdef"
|
||||||
|
)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
conf *RawRequestConfig
|
||||||
|
checkValue checkFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
conf: &RawRequestConfig{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/",
|
||||||
|
Headers: make(map[string]string),
|
||||||
|
},
|
||||||
|
checkValue: func(r *http.Request) {
|
||||||
|
got := r.Method
|
||||||
|
expected := http.MethodPost
|
||||||
|
|
||||||
|
if !(got == expected) {
|
||||||
|
t.Errorf("test failed, got %s, expected %s", got, expected)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conf: &RawRequestConfig{
|
||||||
|
Method: "abcd",
|
||||||
|
Path: "/",
|
||||||
|
Headers: make(map[string]string),
|
||||||
|
},
|
||||||
|
checkValue: func(r *http.Request) {
|
||||||
|
got := r.Method
|
||||||
|
expected := "abcd"
|
||||||
|
|
||||||
|
if !(got == expected) {
|
||||||
|
t.Errorf("test failed, got %s, expected %s", got, expected)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conf: &RawRequestConfig{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/{{payload}}",
|
||||||
|
Headers: make(map[string]string),
|
||||||
|
},
|
||||||
|
checkValue: func(r *http.Request) {
|
||||||
|
got := r.URL.Path
|
||||||
|
expected := "/" + testPayload
|
||||||
|
|
||||||
|
if !(got == expected) {
|
||||||
|
t.Errorf("test failed, got %s, expected %s", got, expected)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conf: &RawRequestConfig{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/test?a={{payload}}",
|
||||||
|
Headers: make(map[string]string),
|
||||||
|
},
|
||||||
|
checkValue: func(r *http.Request) {
|
||||||
|
got := r.URL.Query().Encode()
|
||||||
|
expected := "a=" + testPayload
|
||||||
|
|
||||||
|
if !(got == expected) {
|
||||||
|
t.Errorf("test failed, got %s, expected %s", got, expected)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conf: &RawRequestConfig{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/",
|
||||||
|
Headers: map[string]string{
|
||||||
|
"X-Test-Header": "Test Header {{payload}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
checkValue: func(r *http.Request) {
|
||||||
|
got := r.Header.Get("X-Test-Header")
|
||||||
|
expected := "Test Header " + testPayload
|
||||||
|
|
||||||
|
if !(got == expected) {
|
||||||
|
t.Errorf("test failed, got %s, expected %s", got, expected)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conf: &RawRequestConfig{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/",
|
||||||
|
Headers: make(map[string]string),
|
||||||
|
Body: "Test body {{payload}}",
|
||||||
|
},
|
||||||
|
checkValue: func(r *http.Request) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
b, _ := io.ReadAll(r.Body)
|
||||||
|
|
||||||
|
got := string(b)
|
||||||
|
expected := "Test body " + testPayload
|
||||||
|
|
||||||
|
if !(got == expected) {
|
||||||
|
t.Errorf("test failed, got %s, expected %s", got, expected)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conf: &RawRequestConfig{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/",
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Content-Type": "multipart/form-data; boundary=boundary",
|
||||||
|
},
|
||||||
|
Body: `--boundary
|
||||||
|
Content-disposition: form-data; name="field1"
|
||||||
|
|
||||||
|
Test
|
||||||
|
--boundary
|
||||||
|
Content-disposition: form-data; name="field2"
|
||||||
|
Content-Type: text/plain; charset=utf-7
|
||||||
|
|
||||||
|
Knock knock.
|
||||||
|
{{payload}}
|
||||||
|
--boundary--`,
|
||||||
|
},
|
||||||
|
checkValue: func(r *http.Request) {
|
||||||
|
err := r.ParseMultipartForm(0)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("got error: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
got := r.FormValue("field1")
|
||||||
|
expected := "Test"
|
||||||
|
|
||||||
|
if !(got == expected) {
|
||||||
|
t.Errorf("test failed, got %s, expected %s", got, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
got = r.FormValue("field2")
|
||||||
|
expected = "Knock knock.\n" + testPayload
|
||||||
|
|
||||||
|
if !(got == expected) {
|
||||||
|
t.Errorf("test failed, got %s, expected %s", got, expected)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
req, err := DefaultRawRequest.CreateRequest(url, testPayload, test.conf, types.GoHTTPClient)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("got an error: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
r, ok := req.(*types.GoHTTPRequest)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad request type: %T, expected %T", req, &types.GoHTTPRequest{})
|
||||||
|
}
|
||||||
|
|
||||||
|
test.checkValue(r.Req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/clients/chrome/helpers"
|
||||||
|
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Placeholder = (*RequestBody)(nil)
|
||||||
|
|
||||||
|
var DefaultRequestBody = &RequestBody{name: "RequestBody"}
|
||||||
|
|
||||||
|
type RequestBody struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RequestBody) NewPlaceholderConfig(map[any]any) (PlaceholderConfig, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RequestBody) GetName() string {
|
||||||
|
return p.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RequestBody) CreateRequest(requestURL, payload string, config PlaceholderConfig, httpClientType types.HTTPClientType) (types.Request, error) {
|
||||||
|
reqURL, err := url.Parse(requestURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch httpClientType {
|
||||||
|
case types.GoHTTPClient:
|
||||||
|
return p.prepareGoHTTPClientRequest(reqURL.String(), payload, config)
|
||||||
|
case types.ChromeHTTPClient:
|
||||||
|
return p.prepareChromeHTTPClientRequest(reqURL.String(), payload, config)
|
||||||
|
default:
|
||||||
|
return nil, types.NewUnknownHTTPClientError(httpClientType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RequestBody) prepareGoHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.GoHTTPRequest, error) {
|
||||||
|
// check if we need to set Content-Length manually here
|
||||||
|
req, err := http.NewRequest(http.MethodPost, requestURL, strings.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.GoHTTPRequest{Req: req}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RequestBody) prepareChromeHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.ChromeDPTasks, error) {
|
||||||
|
reqOptions := &helpers.RequestOptions{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Body: fmt.Sprintf(`"%s"`, template.JSEscaper(payload)),
|
||||||
|
}
|
||||||
|
|
||||||
|
task, responseMeta, err := helpers.GetFetchRequest(requestURL, reqOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := &types.ChromeDPTasks{
|
||||||
|
Tasks: chromedp.Tasks{task},
|
||||||
|
ResponseMeta: responseMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/clients/chrome/helpers"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/payload/encoder"
|
||||||
|
)
|
||||||
|
|
||||||
|
const soapBodyPayloadWrapper = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
|
||||||
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<soapenv:Header>
|
||||||
|
<ns1:RequestHeader soapenv:actor="http://schemas.xmlsoap.org/soap/actor/next"
|
||||||
|
soapenv:mustUnderstand="0" xmlns:ns1="https://www.google.com/apis/ads/publisher/v202002">
|
||||||
|
</ns1:RequestHeader>
|
||||||
|
</soapenv:Header>
|
||||||
|
<soapenv:Body>
|
||||||
|
<getAdUnitsByStatement xmlns="https://www.google.com/apis/ads/publisher/v202002">
|
||||||
|
<filterStatement>
|
||||||
|
<%s>%s</%s>
|
||||||
|
</filterStatement>
|
||||||
|
</getAdUnitsByStatement>
|
||||||
|
</soapenv:Body>
|
||||||
|
</soapenv:Envelope>`
|
||||||
|
|
||||||
|
var _ Placeholder = (*SOAPBody)(nil)
|
||||||
|
|
||||||
|
var DefaultSOAPBody = &SOAPBody{name: "SOAPBody"}
|
||||||
|
|
||||||
|
type SOAPBody struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SOAPBody) NewPlaceholderConfig(map[any]any) (PlaceholderConfig, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SOAPBody) GetName() string {
|
||||||
|
return p.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SOAPBody) CreateRequest(requestURL, payload string, config PlaceholderConfig, httpClientType types.HTTPClientType) (types.Request, error) {
|
||||||
|
reqURL, err := url.Parse(requestURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
param, err := RandomHex(Seed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
param = "ab" + param
|
||||||
|
|
||||||
|
encodedPayload, err := encoder.Apply("XMLEntity", payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
soapPayload := fmt.Sprintf(soapBodyPayloadWrapper, param, encodedPayload, param)
|
||||||
|
|
||||||
|
switch httpClientType {
|
||||||
|
case types.GoHTTPClient:
|
||||||
|
return p.prepareGoHTTPClientRequest(reqURL.String(), soapPayload, config)
|
||||||
|
case types.ChromeHTTPClient:
|
||||||
|
return p.prepareChromeHTTPClientRequest(reqURL.String(), soapPayload, config)
|
||||||
|
default:
|
||||||
|
return nil, types.NewUnknownHTTPClientError(httpClientType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SOAPBody) prepareGoHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.GoHTTPRequest, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodPost, requestURL, strings.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("SOAPAction", `"http://schemas.xmlsoap.org/soap/actor/next"`)
|
||||||
|
req.Header.Add("Content-Type", "text/xml")
|
||||||
|
|
||||||
|
return &types.GoHTTPRequest{Req: req}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SOAPBody) prepareChromeHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.ChromeDPTasks, error) {
|
||||||
|
reqOptions := &helpers.RequestOptions{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"SOAPAction": `"http://schemas.xmlsoap.org/soap/actor/next"`,
|
||||||
|
"Content-Type": "text/xml",
|
||||||
|
},
|
||||||
|
Body: fmt.Sprintf(`"%s"`, template.JSEscaper(payload)),
|
||||||
|
}
|
||||||
|
|
||||||
|
task, responseMeta, err := helpers.GetFetchRequest(requestURL, reqOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := &types.ChromeDPTasks{
|
||||||
|
Tasks: chromedp.Tasks{task},
|
||||||
|
ResponseMeta: responseMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/clients/chrome/helpers"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Placeholder = (*URLParam)(nil)
|
||||||
|
|
||||||
|
var DefaultURLParam = &URLParam{name: "URLParam"}
|
||||||
|
|
||||||
|
type URLParam struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *URLParam) NewPlaceholderConfig(map[any]any) (PlaceholderConfig, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *URLParam) GetName() string {
|
||||||
|
return p.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *URLParam) CreateRequest(requestURL, payload string, config PlaceholderConfig, httpClientType types.HTTPClientType) (types.Request, error) {
|
||||||
|
reqURL, err := url.Parse(requestURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqURL.Fragment = ""
|
||||||
|
urlWithPayload := reqURL.String()
|
||||||
|
if reqURL.RawQuery == "" {
|
||||||
|
for i := len(urlWithPayload) - 1; i >= 0; i-- {
|
||||||
|
if urlWithPayload[i] != '/' {
|
||||||
|
if strings.HasSuffix(reqURL.Path, urlWithPayload[i:]) {
|
||||||
|
urlWithPayload = urlWithPayload[:i+1] + "?"
|
||||||
|
} else {
|
||||||
|
urlWithPayload = urlWithPayload[:i+1] + "/?"
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
urlWithPayload += "&"
|
||||||
|
}
|
||||||
|
|
||||||
|
param, err := RandomHex(Seed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
urlWithPayload += param + "="
|
||||||
|
|
||||||
|
switch httpClientType {
|
||||||
|
case types.GoHTTPClient:
|
||||||
|
return p.prepareGoHTTPClientRequest(urlWithPayload, payload, config)
|
||||||
|
case types.ChromeHTTPClient:
|
||||||
|
return p.prepareChromeHTTPClientRequest(urlWithPayload, payload, config)
|
||||||
|
default:
|
||||||
|
return nil, types.NewUnknownHTTPClientError(httpClientType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *URLParam) prepareGoHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.GoHTTPRequest, error) {
|
||||||
|
requestURL += payload
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, requestURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.GoHTTPRequest{Req: req}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *URLParam) prepareChromeHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.ChromeDPTasks, error) {
|
||||||
|
jsEncodedPayload, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
jsEncodedPayloadStr := strings.Trim(string(jsEncodedPayload), "\"")
|
||||||
|
|
||||||
|
requestURL += jsEncodedPayloadStr
|
||||||
|
|
||||||
|
reqOptions := &helpers.RequestOptions{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
}
|
||||||
|
|
||||||
|
task, responseMeta, err := helpers.GetFetchRequest(requestURL, reqOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := &types.ChromeDPTasks{
|
||||||
|
Tasks: chromedp.Tasks{task},
|
||||||
|
ResponseMeta: responseMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestURLParam(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
requestURL string
|
||||||
|
payload string
|
||||||
|
reqURLregexp string
|
||||||
|
}{
|
||||||
|
{"http://example.com", "hello-world", `http://example\.com/\?[a-f0-9]{10}=hello-world`},
|
||||||
|
{"http://example.com/", "hello-world", `http://example\.com/\?[a-f0-9]{10}=hello-world`},
|
||||||
|
{"http://example.com////", "hello-world", `http://example\.com/\?[a-f0-9]{10}=hello-world`},
|
||||||
|
{"http://example.com/?a=b", "hello-world", `http://example\.com/\?a=b&[a-f0-9]{10}=hello-world`},
|
||||||
|
{"http://example.com/?a=b#abc", "hello-world", `http://example\.com/\?a=b&[a-f0-9]{10}=hello-world`},
|
||||||
|
{"http://example.com/a/b/c", "hello-world", `http://example\.com/a/b/c\?[a-f0-9]{10}=hello-world`},
|
||||||
|
{"http://example.com/a/b/c/", "hello-world", `http://example\.com/a/b/c\?[a-f0-9]{10}=hello-world`},
|
||||||
|
{"http://example.com/a/b/c?a=b", "hello-world", `http://example\.com/a/b/c\?a=b&[a-f0-9]{10}=hello-world`},
|
||||||
|
{"http://example.com/a/b/c?a=b#abc", "hello-world", `http://example\.com/a/b/c\?a=b&[a-f0-9]{10}=hello-world`},
|
||||||
|
{"http://example.com", "%0D%0A%09%2Fhello%25world%23", `http://example\.com/\?[a-f0-9]{10}=%0D%0A%09%2Fhello%25world%23`},
|
||||||
|
{"http://example.com/", "%0D%0A%09%2Fhello%25world%23", `http://example\.com/\?[a-f0-9]{10}=%0D%0A%09%2Fhello%25world%23`},
|
||||||
|
{"http://example.com////", "%0D%0A%09%2Fhello%25world%23", `http://example\.com/\?[a-f0-9]{10}=%0D%0A%09%2Fhello%25world%23`},
|
||||||
|
{"http://example.com/?a=b", "%0D%0A%09%2Fhello%25world%23", `http://example\.com/\?a=b&[a-f0-9]{10}=%0D%0A%09%2Fhello%25world%23`},
|
||||||
|
{"http://example.com/?a=b#abc", "%0D%0A%09%2Fhello%25world%23", `http://example\.com/\?a=b&[a-f0-9]{10}=%0D%0A%09%2Fhello%25world%23`},
|
||||||
|
{"http://example.com/a/b/c", "%0D%0A%09%2Fhello%25world%23", `http://example\.com/a/b/c\?[a-f0-9]{10}=%0D%0A%09%2Fhello%25world%23`},
|
||||||
|
{"http://example.com/a/b/c/", "%0D%0A%09%2Fhello%25world%23", `http://example\.com/a/b/c\?[a-f0-9]{10}=%0D%0A%09%2Fhello%25world%23`},
|
||||||
|
{"http://example.com/a/b/c?a=b", "%0D%0A%09%2Fhello%25world%23", `http://example\.com/a/b/c\?a=b&[a-f0-9]{10}=%0D%0A%09%2Fhello%25world%23`},
|
||||||
|
{"http://example.com/a/b/c?a=b#abc", "%0D%0A%09%2Fhello%25world%23", `http://example\.com/a/b/c\?a=b&[a-f0-9]{10}=%0D%0A%09%2Fhello%25world%23`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
req, err := DefaultURLParam.CreateRequest(test.requestURL, test.payload, nil, types.GoHTTPClient)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("got an error while testing: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, ok := req.(*types.GoHTTPRequest)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad request type: %T, expected %T", req, &types.GoHTTPRequest{})
|
||||||
|
}
|
||||||
|
|
||||||
|
reqURL := r.Req.URL.String()
|
||||||
|
matched, err := regexp.MatchString(test.reqURLregexp, reqURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("got an error while testing: %v", err)
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
t.Fatalf("got %s, want %s", reqURL, test.reqURLregexp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/clients/chrome/helpers"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Placeholder = (*URLPath)(nil)
|
||||||
|
|
||||||
|
var DefaultURLPath = &URLPath{name: "URLPath"}
|
||||||
|
|
||||||
|
type URLPath struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *URLPath) NewPlaceholderConfig(map[any]any) (PlaceholderConfig, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *URLPath) GetName() string {
|
||||||
|
return p.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *URLPath) CreateRequest(requestURL, payload string, config PlaceholderConfig, httpClientType types.HTTPClientType) (types.Request, error) {
|
||||||
|
reqURL, err := url.Parse(requestURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
urlWithPayload := reqURL.String()
|
||||||
|
for i := len(urlWithPayload) - 1; i >= 0; i-- {
|
||||||
|
if urlWithPayload[i] != '/' {
|
||||||
|
urlWithPayload = urlWithPayload[:i+1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
urlWithPayload += "/"
|
||||||
|
|
||||||
|
switch httpClientType {
|
||||||
|
case types.GoHTTPClient:
|
||||||
|
return p.prepareGoHTTPClientRequest(urlWithPayload, payload, config)
|
||||||
|
case types.ChromeHTTPClient:
|
||||||
|
return p.prepareChromeHTTPClientRequest(urlWithPayload, payload, config)
|
||||||
|
default:
|
||||||
|
return nil, types.NewUnknownHTTPClientError(httpClientType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *URLPath) prepareGoHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.GoHTTPRequest, error) {
|
||||||
|
requestURL += payload
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, requestURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.GoHTTPRequest{Req: req}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *URLPath) prepareChromeHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.ChromeDPTasks, error) {
|
||||||
|
jsEncodedPayload, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
jsEncodedPayloadStr := strings.Trim(string(jsEncodedPayload), "\"")
|
||||||
|
|
||||||
|
requestURL += jsEncodedPayloadStr
|
||||||
|
|
||||||
|
reqOptions := &helpers.RequestOptions{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
}
|
||||||
|
|
||||||
|
task, responseMeta, err := helpers.GetFetchRequest(requestURL, reqOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := &types.ChromeDPTasks{
|
||||||
|
Tasks: chromedp.Tasks{task},
|
||||||
|
ResponseMeta: responseMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestURLPath(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
requestURL string
|
||||||
|
payload string
|
||||||
|
reqURL string
|
||||||
|
}{
|
||||||
|
{"http://example.com", "hello-world", "http://example.com/hello-world"},
|
||||||
|
{"http://example.com/", "hello-world", "http://example.com/hello-world"},
|
||||||
|
{"http://example.com////", "hello-world", "http://example.com/hello-world"},
|
||||||
|
{"http://example.com", "/hello-world", "http://example.com//hello-world"},
|
||||||
|
{"http://example.com/", "/hello-world", "http://example.com//hello-world"},
|
||||||
|
{"http://example.com", "%0d%0aSet-Cookie:crlf=injection", "http://example.com/%0d%0aSet-Cookie:crlf=injection"},
|
||||||
|
{"http://example.com/", "%0d%0aSet-Cookie:crlf=injection", "http://example.com/%0d%0aSet-Cookie:crlf=injection"},
|
||||||
|
{"http://example.com", "//%0d%0aSet-Cookie:crlf=injection", "http://example.com///%0d%0aSet-Cookie:crlf=injection"},
|
||||||
|
{"http://example.com/", "//%0d%0aSet-Cookie:crlf=injection", "http://example.com///%0d%0aSet-Cookie:crlf=injection"},
|
||||||
|
{"http://example.com", "//%2f/a/b", "http://example.com///%2f/a/b"},
|
||||||
|
{"http://example.com/", "//%2f/a/b", "http://example.com///%2f/a/b"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
req, err := DefaultURLPath.CreateRequest(test.requestURL, test.payload, nil, types.GoHTTPClient)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("got an error while testing: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, ok := req.(*types.GoHTTPRequest)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad request type: %T, expected %T", req, &types.GoHTTPRequest{})
|
||||||
|
}
|
||||||
|
|
||||||
|
if reqURL := r.Req.URL.String(); reqURL != test.reqURL {
|
||||||
|
t.Fatalf("got %s, want %s", reqURL, test.reqURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/chromedp/cdproto/network"
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/clients/chrome/helpers"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const UAHeader = "User-Agent"
|
||||||
|
|
||||||
|
var _ Placeholder = (*UserAgent)(nil)
|
||||||
|
|
||||||
|
var DefaultUserAgent = &UserAgent{name: "UserAgent"}
|
||||||
|
|
||||||
|
type UserAgent struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UserAgent) NewPlaceholderConfig(map[any]any) (PlaceholderConfig, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UserAgent) GetName() string {
|
||||||
|
return p.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UserAgent) CreateRequest(requestURL, payload string, config PlaceholderConfig, httpClientType types.HTTPClientType) (types.Request, error) {
|
||||||
|
reqURL, err := url.Parse(requestURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch httpClientType {
|
||||||
|
case types.GoHTTPClient:
|
||||||
|
return p.prepareGoHTTPClientRequest(reqURL.String(), payload, config)
|
||||||
|
case types.ChromeHTTPClient:
|
||||||
|
return p.prepareChromeHTTPClientRequest(reqURL.String(), payload, config)
|
||||||
|
default:
|
||||||
|
return nil, types.NewUnknownHTTPClientError(httpClientType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UserAgent) prepareGoHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.GoHTTPRequest, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, requestURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set(UAHeader, payload)
|
||||||
|
|
||||||
|
return &types.GoHTTPRequest{Req: req}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UserAgent) prepareChromeHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.ChromeDPTasks, error) {
|
||||||
|
reqOptions := &helpers.RequestOptions{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
}
|
||||||
|
|
||||||
|
task, responseMeta, err := helpers.GetFetchRequest(requestURL, reqOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := &types.ChromeDPTasks{
|
||||||
|
Tasks: chromedp.Tasks{task},
|
||||||
|
UserAgentHeader: network.Headers{UAHeader: payload, "Test": "test"},
|
||||||
|
ResponseMeta: responseMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserAgent(t *testing.T) {
|
||||||
|
const testUrl = "https://example.com"
|
||||||
|
|
||||||
|
tests := []string{
|
||||||
|
"",
|
||||||
|
"ua1",
|
||||||
|
"ua2",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:101.0) Gecko/20100101 Firefox/101.0",
|
||||||
|
"`curl -L http://\u24BC\u24C4\u24C4\u24BC\u24C1\u24BA.\u24B8\u24C4\u24C2`",
|
||||||
|
"$(printf 'hsab/nib/ e- 4321 1.0.0.721 cn'|rev)",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testUA := range tests {
|
||||||
|
req, err := DefaultUserAgent.CreateRequest(testUrl, testUA, nil, types.GoHTTPClient)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("got an error while testing: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, ok := req.(*types.GoHTTPRequest)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad request type: %T, expected %T", req, &types.GoHTTPRequest{})
|
||||||
|
}
|
||||||
|
|
||||||
|
if reqUA := r.Req.UserAgent(); reqUA != testUA {
|
||||||
|
t.Fatalf("got %s, want %s", reqUA, testUA)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RandomHex(n int) (string, error) {
|
||||||
|
bytes := make([]byte, n)
|
||||||
|
|
||||||
|
if _, err := rand.Read(bytes); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(bytes), nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
package placeholder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/clients/chrome/helpers"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Placeholder = (*XMLBody)(nil)
|
||||||
|
|
||||||
|
var DefaultXMLBody = &XMLBody{name: "XMLBody"}
|
||||||
|
|
||||||
|
type XMLBody struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XMLBody) NewPlaceholderConfig(map[any]any) (PlaceholderConfig, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XMLBody) GetName() string {
|
||||||
|
return p.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XMLBody) CreateRequest(requestURL, payload string, config PlaceholderConfig, httpClientType types.HTTPClientType) (types.Request, error) {
|
||||||
|
reqURL, err := url.Parse(requestURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch httpClientType {
|
||||||
|
case types.GoHTTPClient:
|
||||||
|
return p.prepareGoHTTPClientRequest(reqURL.String(), payload, config)
|
||||||
|
case types.ChromeHTTPClient:
|
||||||
|
return p.prepareChromeHTTPClientRequest(reqURL.String(), payload, config)
|
||||||
|
default:
|
||||||
|
return nil, types.NewUnknownHTTPClientError(httpClientType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XMLBody) prepareGoHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.GoHTTPRequest, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodPost, requestURL, strings.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Content-Type", "application/xml")
|
||||||
|
|
||||||
|
return &types.GoHTTPRequest{Req: req}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XMLBody) prepareChromeHTTPClientRequest(requestURL, payload string, config PlaceholderConfig) (*types.ChromeDPTasks, error) {
|
||||||
|
reqOptions := &helpers.RequestOptions{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Content-Type": "application/xml",
|
||||||
|
},
|
||||||
|
Body: fmt.Sprintf(`"%s"`, template.JSEscaper(payload)),
|
||||||
|
}
|
||||||
|
|
||||||
|
task, responseMeta, err := helpers.GetFetchRequest(requestURL, reqOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := &types.ChromeDPTasks{
|
||||||
|
Tasks: chromedp.Tasks{task},
|
||||||
|
ResponseMeta: responseMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks, nil
|
||||||
|
}
|
||||||
169
AttackSurface/src/gotestwaf/internal/report/chart.go
Normal file
169
AttackSurface/src/gotestwaf/internal/report/chart.go
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
package report
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
emptyIndicator = "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pair struct {
|
||||||
|
blocked int
|
||||||
|
bypassed int
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateCounters counts tests by category.
|
||||||
|
func updateCounters(t *db.TestDetails, counters map[string]map[string]pair, isBlocked bool) {
|
||||||
|
var category string
|
||||||
|
var typ string
|
||||||
|
|
||||||
|
if isApiTest(t.TestSet) {
|
||||||
|
category = "api"
|
||||||
|
} else {
|
||||||
|
category = "app"
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Type == "" {
|
||||||
|
typ = "unknown"
|
||||||
|
} else {
|
||||||
|
typ = strings.ToLower(t.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := counters[category]; !ok {
|
||||||
|
counters[category] = make(map[string]pair)
|
||||||
|
}
|
||||||
|
|
||||||
|
val := counters[category][typ]
|
||||||
|
if isBlocked {
|
||||||
|
val.blocked++
|
||||||
|
} else {
|
||||||
|
val.bypassed++
|
||||||
|
}
|
||||||
|
counters[category][typ] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// getIndicatorsAndItems returns indicators and values for charts.
|
||||||
|
func getIndicatorsAndItems(
|
||||||
|
counters map[string]map[string]pair,
|
||||||
|
category string,
|
||||||
|
) (indicators []string, items []float64) {
|
||||||
|
for testType, val := range counters[category] {
|
||||||
|
percentage := float64(db.CalculatePercentage(val.blocked, val.blocked+val.bypassed))
|
||||||
|
|
||||||
|
indicators = append(indicators, fmt.Sprintf("%s (%.1f%%)", testType, percentage))
|
||||||
|
items = append(items, percentage)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(indicators) {
|
||||||
|
case 0:
|
||||||
|
return nil, nil
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
indicators = []string{
|
||||||
|
indicators[0], emptyIndicator, emptyIndicator,
|
||||||
|
emptyIndicator, emptyIndicator, emptyIndicator,
|
||||||
|
}
|
||||||
|
items = []float64{
|
||||||
|
items[0], 0.0, 0.0,
|
||||||
|
0.0, 0.0, 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
indicators = []string{
|
||||||
|
emptyIndicator, indicators[0], emptyIndicator,
|
||||||
|
emptyIndicator, indicators[1], emptyIndicator,
|
||||||
|
}
|
||||||
|
items = []float64{
|
||||||
|
0.0, items[0], 0.0,
|
||||||
|
0.0, items[1], 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
indicators = []string{
|
||||||
|
indicators[0], emptyIndicator, indicators[1],
|
||||||
|
emptyIndicator, indicators[2], emptyIndicator,
|
||||||
|
}
|
||||||
|
items = []float64{
|
||||||
|
items[0], 0.0, items[1],
|
||||||
|
0.0, items[2], 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
indicators = []string{
|
||||||
|
emptyIndicator, indicators[0],
|
||||||
|
emptyIndicator, indicators[1],
|
||||||
|
emptyIndicator, indicators[2],
|
||||||
|
emptyIndicator, indicators[3],
|
||||||
|
}
|
||||||
|
items = []float64{
|
||||||
|
0.0, items[0],
|
||||||
|
0.0, items[1],
|
||||||
|
0.0, items[2],
|
||||||
|
0.0, items[3],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateChartData generates indicators and their values for JS charts.
|
||||||
|
func generateChartData(s *db.Statistics) (
|
||||||
|
apiIndicators []string, apiItems []float64,
|
||||||
|
appIndicators []string, appItems []float64,
|
||||||
|
) {
|
||||||
|
counters := make(map[string]map[string]pair)
|
||||||
|
|
||||||
|
for _, t := range s.TruePositiveTests.Blocked {
|
||||||
|
updateCounters(t, counters, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range s.TruePositiveTests.Bypasses {
|
||||||
|
updateCounters(t, counters, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, containsApiCat := counters["api"]
|
||||||
|
|
||||||
|
if containsApiCat {
|
||||||
|
// Add gRPC counter if gRPC is unavailable to display it on graphic
|
||||||
|
if !s.IsGrpcAvailable {
|
||||||
|
// gRPC is part of the API Security tests
|
||||||
|
counters["api"]["grpc"] = pair{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add GraphQL counter if GraphQL is unavailable to display it on graphic
|
||||||
|
if !s.IsGraphQLAvailable {
|
||||||
|
// GraphQL is part of the API Security tests
|
||||||
|
counters["api"]["graphql"] = pair{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apiIndicators, apiItems = getIndicatorsAndItems(counters, "api")
|
||||||
|
appIndicators, appItems = getIndicatorsAndItems(counters, "app")
|
||||||
|
|
||||||
|
fixIndicators := func(protocolName string) {
|
||||||
|
for i := 0; i < len(apiIndicators); i++ {
|
||||||
|
if strings.HasPrefix(apiIndicators[i], protocolName) {
|
||||||
|
apiIndicators[i] = protocolName + " (unavailable)"
|
||||||
|
apiItems[i] = float64(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if containsApiCat {
|
||||||
|
// Fix label for gRPC if it is unavailable
|
||||||
|
if !s.IsGrpcAvailable {
|
||||||
|
fixIndicators("grpc")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix label for GraphQL if it is unavailable
|
||||||
|
if !s.IsGraphQLAvailable {
|
||||||
|
fixIndicators("graphql")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
346
AttackSurface/src/gotestwaf/internal/report/console.go
Normal file
346
AttackSurface/src/gotestwaf/internal/report/console.go
Normal file
|
|
@ -0,0 +1,346 @@
|
||||||
|
package report
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/olekukonko/tablewriter"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The minimum length of each column in a console table report.
|
||||||
|
const colMinWidth = 21
|
||||||
|
|
||||||
|
// RenderConsoleReport prints a console report in selected format.
|
||||||
|
func RenderConsoleReport(
|
||||||
|
s *db.Statistics,
|
||||||
|
reportTime time.Time,
|
||||||
|
wafName string,
|
||||||
|
url string,
|
||||||
|
args []string,
|
||||||
|
ignoreUnresolved bool,
|
||||||
|
format string,
|
||||||
|
) error {
|
||||||
|
switch format {
|
||||||
|
case consoleReportTextFormat:
|
||||||
|
printConsoleReportTable(s, reportTime, wafName, ignoreUnresolved)
|
||||||
|
case consoleReportJsonFormat:
|
||||||
|
err := printConsoleReportJson(s, reportTime, wafName, url, args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown report format: %s", format)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// printConsoleReportTable prepare and prints a console report in tabular format.
|
||||||
|
func printConsoleReportTable(
|
||||||
|
s *db.Statistics,
|
||||||
|
reportTime time.Time,
|
||||||
|
wafName string,
|
||||||
|
ignoreUnresolved bool,
|
||||||
|
) {
|
||||||
|
baseHeader := []string{"Test set", "Test case", "Percentage, %", "Blocked", "Bypassed"}
|
||||||
|
if !ignoreUnresolved {
|
||||||
|
baseHeader = append(baseHeader, "Unresolved")
|
||||||
|
}
|
||||||
|
baseHeader = append(baseHeader, "Sent", "Failed")
|
||||||
|
|
||||||
|
var buffer strings.Builder
|
||||||
|
|
||||||
|
fmt.Fprintf(&buffer, "True-Positive Tests:\n")
|
||||||
|
|
||||||
|
// Negative cases summary table
|
||||||
|
table := tablewriter.NewWriter(&buffer)
|
||||||
|
table.Header(baseHeader)
|
||||||
|
|
||||||
|
for _, row := range s.TruePositiveTests.SummaryTable {
|
||||||
|
rowAppend := []string{
|
||||||
|
row.TestSet,
|
||||||
|
row.TestCase,
|
||||||
|
fmt.Sprintf("%.2f", row.Percentage),
|
||||||
|
fmt.Sprintf("%d", row.Blocked),
|
||||||
|
fmt.Sprintf("%d", row.Bypassed),
|
||||||
|
}
|
||||||
|
if !ignoreUnresolved {
|
||||||
|
rowAppend = append(rowAppend, fmt.Sprintf("%d", row.Unresolved))
|
||||||
|
}
|
||||||
|
rowAppend = append(rowAppend,
|
||||||
|
fmt.Sprintf("%d", row.Sent),
|
||||||
|
fmt.Sprintf("%d", row.Failed),
|
||||||
|
)
|
||||||
|
table.Append(rowAppend)
|
||||||
|
}
|
||||||
|
|
||||||
|
footerNegativeTests := []string{
|
||||||
|
fmt.Sprintf("Date:\n%s", reportTime.Format("2006-01-02")),
|
||||||
|
fmt.Sprintf("Project Name:\n%s", wafName),
|
||||||
|
fmt.Sprintf("True-Positive Score:\n%.2f%%", s.TruePositiveTests.ResolvedBlockedRequestsPercentage),
|
||||||
|
fmt.Sprintf("Blocked (Resolved):\n%d/%d (%.2f%%)",
|
||||||
|
s.TruePositiveTests.ReqStats.BlockedRequestsNumber,
|
||||||
|
s.TruePositiveTests.ReqStats.ResolvedRequestsNumber,
|
||||||
|
s.TruePositiveTests.ResolvedBlockedRequestsPercentage,
|
||||||
|
),
|
||||||
|
fmt.Sprintf("Bypassed (Resolved):\n%d/%d (%.2f%%)",
|
||||||
|
s.TruePositiveTests.ReqStats.BypassedRequestsNumber,
|
||||||
|
s.TruePositiveTests.ReqStats.ResolvedRequestsNumber,
|
||||||
|
s.TruePositiveTests.ResolvedBypassedRequestsPercentage,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if !ignoreUnresolved {
|
||||||
|
footerNegativeTests = append(footerNegativeTests,
|
||||||
|
fmt.Sprintf("Unresolved (Sent):\n%d/%d (%.2f%%)",
|
||||||
|
s.TruePositiveTests.ReqStats.UnresolvedRequestsNumber,
|
||||||
|
s.TruePositiveTests.ReqStats.AllRequestsNumber,
|
||||||
|
s.TruePositiveTests.UnresolvedRequestsPercentage,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
footerNegativeTests = append(footerNegativeTests,
|
||||||
|
fmt.Sprintf("Total Sent:\n%d", s.TruePositiveTests.ReqStats.AllRequestsNumber),
|
||||||
|
fmt.Sprintf("Failed (Total):\n%d/%d (%.2f%%)",
|
||||||
|
s.TruePositiveTests.ReqStats.FailedRequestsNumber,
|
||||||
|
s.TruePositiveTests.ReqStats.AllRequestsNumber,
|
||||||
|
s.TruePositiveTests.FailedRequestsPercentage,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
table.Footer(footerNegativeTests)
|
||||||
|
table.Render()
|
||||||
|
|
||||||
|
fmt.Fprintf(&buffer, "\nTrue-Negative Tests:\n")
|
||||||
|
|
||||||
|
// Positive cases summary table
|
||||||
|
posTable := tablewriter.NewWriter(&buffer)
|
||||||
|
posTable.Header(baseHeader)
|
||||||
|
|
||||||
|
for _, row := range s.TrueNegativeTests.SummaryTable {
|
||||||
|
rowAppend := []string{
|
||||||
|
row.TestSet,
|
||||||
|
row.TestCase,
|
||||||
|
fmt.Sprintf("%.2f", row.Percentage),
|
||||||
|
fmt.Sprintf("%d", row.Blocked),
|
||||||
|
fmt.Sprintf("%d", row.Bypassed),
|
||||||
|
}
|
||||||
|
if !ignoreUnresolved {
|
||||||
|
rowAppend = append(rowAppend, fmt.Sprintf("%d", row.Unresolved))
|
||||||
|
}
|
||||||
|
rowAppend = append(rowAppend,
|
||||||
|
fmt.Sprintf("%d", row.Sent),
|
||||||
|
fmt.Sprintf("%d", row.Failed),
|
||||||
|
)
|
||||||
|
posTable.Append(rowAppend)
|
||||||
|
}
|
||||||
|
|
||||||
|
footerPositiveTests := []string{
|
||||||
|
fmt.Sprintf("Date:\n%s", reportTime.Format("2006-01-02")),
|
||||||
|
fmt.Sprintf("Project Name:\n%s", wafName),
|
||||||
|
fmt.Sprintf("True-Negative Score:\n%.2f%%", s.TrueNegativeTests.ResolvedBypassedRequestsPercentage),
|
||||||
|
fmt.Sprintf("Blocked (Resolved):\n%d/%d (%.2f%%)",
|
||||||
|
s.TrueNegativeTests.ReqStats.BlockedRequestsNumber,
|
||||||
|
s.TrueNegativeTests.ReqStats.ResolvedRequestsNumber,
|
||||||
|
s.TrueNegativeTests.ResolvedBlockedRequestsPercentage,
|
||||||
|
),
|
||||||
|
fmt.Sprintf("Bypassed (Resolved):\n%d/%d (%.2f%%)",
|
||||||
|
s.TrueNegativeTests.ReqStats.BypassedRequestsNumber,
|
||||||
|
s.TrueNegativeTests.ReqStats.ResolvedRequestsNumber,
|
||||||
|
s.TrueNegativeTests.ResolvedBypassedRequestsPercentage,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if !ignoreUnresolved {
|
||||||
|
footerPositiveTests = append(footerPositiveTests,
|
||||||
|
fmt.Sprintf("Unresolved (Sent):\n%d/%d (%.2f%%)",
|
||||||
|
s.TrueNegativeTests.ReqStats.UnresolvedRequestsNumber,
|
||||||
|
s.TrueNegativeTests.ReqStats.AllRequestsNumber,
|
||||||
|
s.TrueNegativeTests.UnresolvedRequestsPercentage,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
footerPositiveTests = append(footerPositiveTests,
|
||||||
|
fmt.Sprintf("Total Sent:\n%d", s.TrueNegativeTests.ReqStats.AllRequestsNumber),
|
||||||
|
fmt.Sprintf("Failed (Total):\n%d/%d (%.2f%%)",
|
||||||
|
s.TrueNegativeTests.ReqStats.FailedRequestsNumber,
|
||||||
|
s.TrueNegativeTests.ReqStats.AllRequestsNumber,
|
||||||
|
s.TrueNegativeTests.FailedRequestsPercentage,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
posTable.Footer(footerPositiveTests)
|
||||||
|
posTable.Render()
|
||||||
|
|
||||||
|
fmt.Fprintf(&buffer, "\nSummary:\n")
|
||||||
|
|
||||||
|
// summary table
|
||||||
|
sumTable := tablewriter.NewWriter(&buffer)
|
||||||
|
baseHeader = []string{"Type", "True-Positive tests blocked", "True-Negative tests passed", "Average"}
|
||||||
|
sumTable.Header(baseHeader)
|
||||||
|
|
||||||
|
row := []string{"API Security"}
|
||||||
|
if s.Score.ApiSec.TruePositive != -1.0 {
|
||||||
|
row = append(row, fmt.Sprintf("%.2f%%", s.Score.ApiSec.TruePositive))
|
||||||
|
} else {
|
||||||
|
row = append(row, "n/a")
|
||||||
|
}
|
||||||
|
if s.Score.ApiSec.TrueNegative != -1.0 {
|
||||||
|
row = append(row, fmt.Sprintf("%.2f%%", s.Score.ApiSec.TrueNegative))
|
||||||
|
} else {
|
||||||
|
row = append(row, "n/a")
|
||||||
|
}
|
||||||
|
if s.Score.ApiSec.Average != -1.0 {
|
||||||
|
row = append(row, fmt.Sprintf("%.2f%%", s.Score.ApiSec.Average))
|
||||||
|
} else {
|
||||||
|
row = append(row, "n/a")
|
||||||
|
}
|
||||||
|
sumTable.Append(row)
|
||||||
|
|
||||||
|
row = []string{"Application Security"}
|
||||||
|
if s.Score.AppSec.TruePositive != -1.0 {
|
||||||
|
row = append(row, fmt.Sprintf("%.2f%%", s.Score.AppSec.TruePositive))
|
||||||
|
} else {
|
||||||
|
row = append(row, "n/a")
|
||||||
|
}
|
||||||
|
if s.Score.AppSec.TrueNegative != -1.0 {
|
||||||
|
row = append(row, fmt.Sprintf("%.2f%%", s.Score.AppSec.TrueNegative))
|
||||||
|
} else {
|
||||||
|
row = append(row, "n/a")
|
||||||
|
}
|
||||||
|
if s.Score.AppSec.Average != -1.0 {
|
||||||
|
row = append(row, fmt.Sprintf("%.2f%%", s.Score.AppSec.Average))
|
||||||
|
} else {
|
||||||
|
row = append(row, "n/a")
|
||||||
|
}
|
||||||
|
sumTable.Append(row)
|
||||||
|
|
||||||
|
footer := []string{"", "", "Score"}
|
||||||
|
if s.Score.Average != -1.0 {
|
||||||
|
footer = append(footer, fmt.Sprintf("%.2f%%", s.Score.Average))
|
||||||
|
} else {
|
||||||
|
footer = append(footer, "n/a")
|
||||||
|
}
|
||||||
|
sumTable.Footer(footer)
|
||||||
|
sumTable.Render()
|
||||||
|
|
||||||
|
fmt.Println(buffer.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// printConsoleReportJson prepares and prints a console report in json format.
|
||||||
|
func printConsoleReportJson(
|
||||||
|
s *db.Statistics,
|
||||||
|
reportTime time.Time,
|
||||||
|
wafName string,
|
||||||
|
url string,
|
||||||
|
args []string,
|
||||||
|
) error {
|
||||||
|
report := jsonReport{
|
||||||
|
Date: reportTime.Format(time.ANSIC),
|
||||||
|
ProjectName: wafName,
|
||||||
|
URL: url,
|
||||||
|
TestCasesFP: s.TestCasesFingerprint,
|
||||||
|
Args: strings.Join(args, " "),
|
||||||
|
Score: s.Score.Average,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.TruePositiveTests.SummaryTable) != 0 {
|
||||||
|
report.TruePositiveTests = &testsInfo{
|
||||||
|
Score: s.TruePositiveTests.ResolvedBlockedRequestsPercentage,
|
||||||
|
Summary: requestStats{
|
||||||
|
TotalSent: s.TruePositiveTests.ReqStats.AllRequestsNumber,
|
||||||
|
ResolvedTests: s.TruePositiveTests.ReqStats.ResolvedRequestsNumber,
|
||||||
|
BlockedTests: s.TruePositiveTests.ReqStats.BlockedRequestsNumber,
|
||||||
|
BypassedTests: s.TruePositiveTests.ReqStats.BypassedRequestsNumber,
|
||||||
|
UnresolvedTests: s.TruePositiveTests.ReqStats.UnresolvedRequestsNumber,
|
||||||
|
FailedTests: s.TruePositiveTests.ReqStats.FailedRequestsNumber,
|
||||||
|
},
|
||||||
|
ApiSecStat: requestStats{
|
||||||
|
TotalSent: s.TruePositiveTests.ApiSecReqStats.AllRequestsNumber,
|
||||||
|
ResolvedTests: s.TruePositiveTests.ApiSecReqStats.ResolvedRequestsNumber,
|
||||||
|
BlockedTests: s.TruePositiveTests.ApiSecReqStats.BlockedRequestsNumber,
|
||||||
|
BypassedTests: s.TruePositiveTests.ApiSecReqStats.BypassedRequestsNumber,
|
||||||
|
UnresolvedTests: s.TruePositiveTests.ApiSecReqStats.UnresolvedRequestsNumber,
|
||||||
|
FailedTests: s.TruePositiveTests.ApiSecReqStats.FailedRequestsNumber,
|
||||||
|
},
|
||||||
|
AppSecStat: requestStats{
|
||||||
|
TotalSent: s.TruePositiveTests.AppSecReqStats.AllRequestsNumber,
|
||||||
|
ResolvedTests: s.TruePositiveTests.AppSecReqStats.ResolvedRequestsNumber,
|
||||||
|
BlockedTests: s.TruePositiveTests.AppSecReqStats.BlockedRequestsNumber,
|
||||||
|
BypassedTests: s.TruePositiveTests.AppSecReqStats.BypassedRequestsNumber,
|
||||||
|
UnresolvedTests: s.TruePositiveTests.AppSecReqStats.UnresolvedRequestsNumber,
|
||||||
|
FailedTests: s.TruePositiveTests.AppSecReqStats.FailedRequestsNumber,
|
||||||
|
},
|
||||||
|
TestSets: make(testSets),
|
||||||
|
}
|
||||||
|
for _, row := range s.TruePositiveTests.SummaryTable {
|
||||||
|
if report.TruePositiveTests.TestSets[row.TestSet] == nil {
|
||||||
|
report.TruePositiveTests.TestSets[row.TestSet] = make(testCases)
|
||||||
|
}
|
||||||
|
report.TruePositiveTests.TestSets[row.TestSet][row.TestCase] = &testCaseInfo{
|
||||||
|
Percentage: row.Percentage,
|
||||||
|
Sent: row.Sent,
|
||||||
|
Blocked: row.Blocked,
|
||||||
|
Bypassed: row.Bypassed,
|
||||||
|
Unresolved: row.Unresolved,
|
||||||
|
Failed: row.Failed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.TrueNegativeTests.SummaryTable) != 0 {
|
||||||
|
report.TrueNegativeTests = &testsInfo{
|
||||||
|
Score: s.TrueNegativeTests.ResolvedBypassedRequestsPercentage,
|
||||||
|
Summary: requestStats{
|
||||||
|
TotalSent: s.TrueNegativeTests.ReqStats.AllRequestsNumber,
|
||||||
|
ResolvedTests: s.TrueNegativeTests.ReqStats.ResolvedRequestsNumber,
|
||||||
|
BlockedTests: s.TrueNegativeTests.ReqStats.BlockedRequestsNumber,
|
||||||
|
BypassedTests: s.TrueNegativeTests.ReqStats.BypassedRequestsNumber,
|
||||||
|
UnresolvedTests: s.TrueNegativeTests.ReqStats.UnresolvedRequestsNumber,
|
||||||
|
FailedTests: s.TrueNegativeTests.ReqStats.FailedRequestsNumber,
|
||||||
|
},
|
||||||
|
ApiSecStat: requestStats{
|
||||||
|
TotalSent: s.TrueNegativeTests.ApiSecReqStats.AllRequestsNumber,
|
||||||
|
ResolvedTests: s.TrueNegativeTests.ApiSecReqStats.ResolvedRequestsNumber,
|
||||||
|
BlockedTests: s.TrueNegativeTests.ApiSecReqStats.BlockedRequestsNumber,
|
||||||
|
BypassedTests: s.TrueNegativeTests.ApiSecReqStats.BypassedRequestsNumber,
|
||||||
|
UnresolvedTests: s.TrueNegativeTests.ApiSecReqStats.UnresolvedRequestsNumber,
|
||||||
|
FailedTests: s.TrueNegativeTests.ApiSecReqStats.FailedRequestsNumber,
|
||||||
|
},
|
||||||
|
AppSecStat: requestStats{
|
||||||
|
TotalSent: s.TrueNegativeTests.AppSecReqStats.AllRequestsNumber,
|
||||||
|
ResolvedTests: s.TrueNegativeTests.AppSecReqStats.ResolvedRequestsNumber,
|
||||||
|
BlockedTests: s.TrueNegativeTests.AppSecReqStats.BlockedRequestsNumber,
|
||||||
|
BypassedTests: s.TrueNegativeTests.AppSecReqStats.BypassedRequestsNumber,
|
||||||
|
UnresolvedTests: s.TrueNegativeTests.AppSecReqStats.UnresolvedRequestsNumber,
|
||||||
|
FailedTests: s.TrueNegativeTests.AppSecReqStats.FailedRequestsNumber,
|
||||||
|
},
|
||||||
|
TestSets: make(testSets),
|
||||||
|
}
|
||||||
|
for _, row := range s.TrueNegativeTests.SummaryTable {
|
||||||
|
if report.TrueNegativeTests.TestSets[row.TestSet] == nil {
|
||||||
|
report.TrueNegativeTests.TestSets[row.TestSet] = make(testCases)
|
||||||
|
}
|
||||||
|
report.TrueNegativeTests.TestSets[row.TestSet][row.TestCase] = &testCaseInfo{
|
||||||
|
Percentage: row.Percentage,
|
||||||
|
Sent: row.Sent,
|
||||||
|
Blocked: row.Blocked,
|
||||||
|
Bypassed: row.Bypassed,
|
||||||
|
Unresolved: row.Unresolved,
|
||||||
|
Failed: row.Failed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBytes, err := json.Marshal(report)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't export report to JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(string(jsonBytes))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
80
AttackSurface/src/gotestwaf/internal/report/email.go
Normal file
80
AttackSurface/src/gotestwaf/internal/report/email.go
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
package report
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/pkg/report"
|
||||||
|
)
|
||||||
|
|
||||||
|
const serverURL = "https://gotestwaf.wallarm.tools/v1/send-report"
|
||||||
|
|
||||||
|
var _ error = (*ErrorResponse)(nil)
|
||||||
|
|
||||||
|
type ErrorResponse struct {
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrorResponse) Error() string {
|
||||||
|
return e.Msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendEmail(ctx context.Context, reportData *report.HtmlReport, email string) error {
|
||||||
|
requestUrl, err := url.Parse(serverURL)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't parse server URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParams := requestUrl.Query()
|
||||||
|
queryParams.Set("email", email)
|
||||||
|
requestUrl.RawQuery = queryParams.Encode()
|
||||||
|
|
||||||
|
data, err := json.Marshal(reportData)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't marshal report data into JSON format")
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestUrl.String(), bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't create request")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't send request to server")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusOK:
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case http.StatusBadRequest,
|
||||||
|
http.StatusRequestEntityTooLarge,
|
||||||
|
http.StatusTooManyRequests,
|
||||||
|
http.StatusInternalServerError:
|
||||||
|
|
||||||
|
var errResp ErrorResponse
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't read error message from server")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, &errResp); err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't parse error message from server")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &errResp
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("bad status code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
8
AttackSurface/src/gotestwaf/internal/report/helpers.go
Normal file
8
AttackSurface/src/gotestwaf/internal/report/helpers.go
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
package report
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// isApiTest checks if a set of tests has the API category.
|
||||||
|
func isApiTest(setName string) bool {
|
||||||
|
return strings.Contains(setName, "api")
|
||||||
|
}
|
||||||
487
AttackSurface/src/gotestwaf/internal/report/html.go
Normal file
487
AttackSurface/src/gotestwaf/internal/report/html.go
Normal file
|
|
@ -0,0 +1,487 @@
|
||||||
|
package report
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/db"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/helpers"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/version"
|
||||||
|
"github.com/wallarm/gotestwaf/pkg/report"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
naMark = "N/A"
|
||||||
|
|
||||||
|
maxUntruncatedPayloadLength = 1100
|
||||||
|
truncatedPartsLength = 150
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
prepareHTMLReportOnce sync.Once
|
||||||
|
htmlReportData *report.HtmlReport
|
||||||
|
|
||||||
|
comparisonTable = []*report.ComparisonTableRow{
|
||||||
|
{
|
||||||
|
Name: "ModSecurity PARANOIA=1",
|
||||||
|
ApiSec: computeGrade(42.86, 1),
|
||||||
|
AppSec: computeGrade(68.16, 1),
|
||||||
|
OverallScore: computeGrade(55.51, 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ModSecurity PARANOIA=2",
|
||||||
|
ApiSec: computeGrade(57.14, 1),
|
||||||
|
AppSec: computeGrade(60.12, 1),
|
||||||
|
OverallScore: computeGrade(58.63, 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ModSecurity PARANOIA=3",
|
||||||
|
ApiSec: computeGrade(78.57, 1),
|
||||||
|
AppSec: computeGrade(51.24, 1),
|
||||||
|
OverallScore: computeGrade(64.91, 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ModSecurity PARANOIA=4",
|
||||||
|
ApiSec: computeGrade(100.00, 1),
|
||||||
|
AppSec: computeGrade(36.74, 1),
|
||||||
|
OverallScore: computeGrade(68.37, 1),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
wallarmResult = &report.ComparisonTableRow{
|
||||||
|
Name: "Wallarm",
|
||||||
|
ApiSec: computeGrade(100, 1),
|
||||||
|
AppSec: computeGrade(97.74, 1),
|
||||||
|
OverallScore: computeGrade(98.87, 1),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func getGrade(grade float64, na bool) *report.Grade {
|
||||||
|
g := &report.Grade{
|
||||||
|
Percentage: 0.0,
|
||||||
|
Mark: naMark,
|
||||||
|
CSSClassSuffix: "na",
|
||||||
|
}
|
||||||
|
|
||||||
|
if na {
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Percentage = grade
|
||||||
|
if g.Percentage <= 1 {
|
||||||
|
g.Percentage *= 100
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case g.Percentage >= 97.0:
|
||||||
|
g.Mark = "A+"
|
||||||
|
g.CSSClassSuffix = "a"
|
||||||
|
case g.Percentage >= 93.0:
|
||||||
|
g.Mark = "A"
|
||||||
|
g.CSSClassSuffix = "a"
|
||||||
|
case g.Percentage >= 90.0:
|
||||||
|
g.Mark = "A-"
|
||||||
|
g.CSSClassSuffix = "a"
|
||||||
|
case g.Percentage >= 87.0:
|
||||||
|
g.Mark = "B+"
|
||||||
|
g.CSSClassSuffix = "b"
|
||||||
|
case g.Percentage >= 83.0:
|
||||||
|
g.Mark = "B"
|
||||||
|
g.CSSClassSuffix = "b"
|
||||||
|
case g.Percentage >= 80.0:
|
||||||
|
g.Mark = "B-"
|
||||||
|
g.CSSClassSuffix = "b"
|
||||||
|
case g.Percentage >= 77.0:
|
||||||
|
g.Mark = "C+"
|
||||||
|
g.CSSClassSuffix = "c"
|
||||||
|
case g.Percentage >= 73.0:
|
||||||
|
g.Mark = "C"
|
||||||
|
g.CSSClassSuffix = "c"
|
||||||
|
case g.Percentage >= 70.0:
|
||||||
|
g.Mark = "C-"
|
||||||
|
g.CSSClassSuffix = "c"
|
||||||
|
case g.Percentage >= 67.0:
|
||||||
|
g.Mark = "D+"
|
||||||
|
g.CSSClassSuffix = "d"
|
||||||
|
case g.Percentage >= 63.0:
|
||||||
|
g.Mark = "D"
|
||||||
|
g.CSSClassSuffix = "d"
|
||||||
|
case g.Percentage >= 60.0:
|
||||||
|
g.Mark = "D-"
|
||||||
|
g.CSSClassSuffix = "d"
|
||||||
|
case g.Percentage < 60.0:
|
||||||
|
g.Mark = "F"
|
||||||
|
g.CSSClassSuffix = "f"
|
||||||
|
}
|
||||||
|
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeGrade(value float64, all int) *report.Grade {
|
||||||
|
if all == 0 {
|
||||||
|
return getGrade(0.0, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return getGrade(value/float64(all), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// truncatePayload replaces the middle part of the payload if
|
||||||
|
// it is longer than maxUntruncatedPayloadLength.
|
||||||
|
func truncatePayload(payload string) string {
|
||||||
|
payloadLength := len(payload)
|
||||||
|
|
||||||
|
if payloadLength <= maxUntruncatedPayloadLength {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
truncatedLength := payloadLength - 2*truncatedPartsLength
|
||||||
|
|
||||||
|
truncatedPayload := fmt.Sprintf(
|
||||||
|
"%s … truncated %d symbols … %s",
|
||||||
|
payload[:truncatedPartsLength],
|
||||||
|
truncatedLength,
|
||||||
|
payload[payloadLength-truncatedPartsLength:],
|
||||||
|
)
|
||||||
|
|
||||||
|
return truncatedPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareHTMLFullReport prepares ready data to insert into the HTML template.
|
||||||
|
func prepareHTMLFullReport(
|
||||||
|
s *db.Statistics, reportTime time.Time, wafName string,
|
||||||
|
url string, openApiFile string, args []string, ignoreUnresolved bool, includePayloads bool,
|
||||||
|
) (*report.HtmlReport, error) {
|
||||||
|
data := &report.HtmlReport{
|
||||||
|
IgnoreUnresolved: ignoreUnresolved,
|
||||||
|
IncludePayloads: includePayloads,
|
||||||
|
WafName: wafName,
|
||||||
|
Url: url,
|
||||||
|
WafTestingDate: reportTime.Format("02 January 2006"),
|
||||||
|
GtwVersion: version.Version,
|
||||||
|
TestCasesFP: s.TestCasesFingerprint,
|
||||||
|
OpenApiFile: openApiFile,
|
||||||
|
Args: args,
|
||||||
|
ComparisonTable: comparisonTable,
|
||||||
|
WallarmResult: wallarmResult,
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Score.ApiSec.TruePositive < 0 {
|
||||||
|
data.ApiSec.TruePositiveTestsGrade = getGrade(0.0, true)
|
||||||
|
} else {
|
||||||
|
data.ApiSec.TruePositiveTestsGrade = getGrade(s.Score.ApiSec.TruePositive, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Score.ApiSec.TrueNegative < 0 {
|
||||||
|
data.ApiSec.TrueNegativeTestsGrade = getGrade(0.0, true)
|
||||||
|
} else {
|
||||||
|
data.ApiSec.TrueNegativeTestsGrade = getGrade(s.Score.ApiSec.TrueNegative, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Score.ApiSec.Average < 0 {
|
||||||
|
data.ApiSec.Grade = getGrade(0.0, true)
|
||||||
|
} else {
|
||||||
|
data.ApiSec.Grade = getGrade(s.Score.ApiSec.Average, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Score.AppSec.TruePositive < 0 {
|
||||||
|
data.AppSec.TruePositiveTestsGrade = getGrade(0.0, true)
|
||||||
|
} else {
|
||||||
|
data.AppSec.TruePositiveTestsGrade = getGrade(s.Score.AppSec.TruePositive, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Score.AppSec.TrueNegative < 0 {
|
||||||
|
data.AppSec.TrueNegativeTestsGrade = getGrade(0.0, true)
|
||||||
|
} else {
|
||||||
|
data.AppSec.TrueNegativeTestsGrade = getGrade(s.Score.AppSec.TrueNegative, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Score.AppSec.Average < 0 {
|
||||||
|
data.AppSec.Grade = getGrade(0.0, true)
|
||||||
|
} else {
|
||||||
|
data.AppSec.Grade = getGrade(s.Score.AppSec.Average, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Overall = getGrade(s.Score.Average, false)
|
||||||
|
|
||||||
|
apiIndicators, apiItems, appIndicators, appItems := generateChartData(s)
|
||||||
|
|
||||||
|
data.ApiSecChartData.Indicators = apiIndicators
|
||||||
|
data.ApiSecChartData.Items = apiItems
|
||||||
|
data.AppSecChartData.Indicators = appIndicators
|
||||||
|
data.AppSecChartData.Items = appItems
|
||||||
|
|
||||||
|
data.TruePositiveTests.SummaryTable = make(map[string]*report.TestSetSummary)
|
||||||
|
for _, row := range s.TruePositiveTests.SummaryTable {
|
||||||
|
if _, ok := data.TruePositiveTests.SummaryTable[row.TestSet]; !ok {
|
||||||
|
data.TruePositiveTests.SummaryTable[row.TestSet] = &report.TestSetSummary{}
|
||||||
|
}
|
||||||
|
|
||||||
|
testSetSum := data.TruePositiveTests.SummaryTable[row.TestSet]
|
||||||
|
|
||||||
|
testSetSum.TestCases = append(testSetSum.TestCases, row)
|
||||||
|
|
||||||
|
testSetSum.Sent += row.Sent
|
||||||
|
testSetSum.Blocked += row.Blocked
|
||||||
|
testSetSum.Bypassed += row.Bypassed
|
||||||
|
testSetSum.Unresolved += row.Unresolved
|
||||||
|
testSetSum.Failed += row.Failed
|
||||||
|
|
||||||
|
if row.Blocked+row.Bypassed != 0 {
|
||||||
|
testSetSum.ResolvedTestCasesNumber += 1
|
||||||
|
testSetSum.Percentage += row.Percentage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, testSetSum := range data.TruePositiveTests.SummaryTable {
|
||||||
|
testSetSum.Percentage = db.Round(testSetSum.Percentage / float64(testSetSum.ResolvedTestCasesNumber))
|
||||||
|
}
|
||||||
|
|
||||||
|
data.TrueNegativeTests.SummaryTable = make(map[string]*report.TestSetSummary)
|
||||||
|
for _, row := range s.TrueNegativeTests.SummaryTable {
|
||||||
|
if _, ok := data.TrueNegativeTests.SummaryTable[row.TestSet]; !ok {
|
||||||
|
data.TrueNegativeTests.SummaryTable[row.TestSet] = &report.TestSetSummary{}
|
||||||
|
}
|
||||||
|
|
||||||
|
testSetSum := data.TrueNegativeTests.SummaryTable[row.TestSet]
|
||||||
|
|
||||||
|
testSetSum.TestCases = append(testSetSum.TestCases, row)
|
||||||
|
|
||||||
|
testSetSum.Sent += row.Sent
|
||||||
|
testSetSum.Blocked += row.Blocked
|
||||||
|
testSetSum.Bypassed += row.Bypassed
|
||||||
|
testSetSum.Unresolved += row.Unresolved
|
||||||
|
testSetSum.Failed += row.Failed
|
||||||
|
|
||||||
|
if row.Blocked+row.Bypassed != 0 {
|
||||||
|
testSetSum.ResolvedTestCasesNumber += 1
|
||||||
|
testSetSum.Percentage += row.Percentage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, testSetSum := range data.TrueNegativeTests.SummaryTable {
|
||||||
|
testSetSum.Percentage = db.Round(testSetSum.Percentage / float64(testSetSum.ResolvedTestCasesNumber))
|
||||||
|
}
|
||||||
|
|
||||||
|
if includePayloads {
|
||||||
|
// map[paths]map[payload]map[statusCode]*testDetails
|
||||||
|
negBypassed := make(map[string]map[string]map[int]*report.TestDetails)
|
||||||
|
for _, d := range s.TruePositiveTests.Bypasses {
|
||||||
|
paths := strings.Join(d.AdditionalInfo, "\n")
|
||||||
|
|
||||||
|
if _, ok := negBypassed[paths]; !ok {
|
||||||
|
// map[payload]map[statusCode]*testDetails
|
||||||
|
negBypassed[paths] = make(map[string]map[int]*report.TestDetails)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := truncatePayload(d.Payload)
|
||||||
|
|
||||||
|
if _, ok := negBypassed[paths][payload]; !ok {
|
||||||
|
// map[statusCode]*testDetails
|
||||||
|
negBypassed[paths][payload] = make(map[int]*report.TestDetails)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := negBypassed[paths][payload][d.ResponseStatusCode]; !ok {
|
||||||
|
negBypassed[paths][payload][d.ResponseStatusCode] = &report.TestDetails{
|
||||||
|
Encoders: make(map[string]any),
|
||||||
|
Placeholders: make(map[string]any),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
negBypassed[paths][payload][d.ResponseStatusCode].TestCase = d.TestCase
|
||||||
|
negBypassed[paths][payload][d.ResponseStatusCode].Encoders[d.Encoder] = nil
|
||||||
|
negBypassed[paths][payload][d.ResponseStatusCode].Placeholders[d.Placeholder] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// map[payload]map[statusCode]*testDetails
|
||||||
|
negUnresolved := make(map[string]map[int]*report.TestDetails)
|
||||||
|
for _, d := range s.TruePositiveTests.Unresolved {
|
||||||
|
payload := truncatePayload(d.Payload)
|
||||||
|
|
||||||
|
if _, ok := negUnresolved[payload]; !ok {
|
||||||
|
// map[statusCode]*testDetails
|
||||||
|
negUnresolved[payload] = make(map[int]*report.TestDetails)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := negUnresolved[payload][d.ResponseStatusCode]; !ok {
|
||||||
|
negUnresolved[payload][d.ResponseStatusCode] = &report.TestDetails{
|
||||||
|
Encoders: make(map[string]any),
|
||||||
|
Placeholders: make(map[string]any),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
negUnresolved[payload][d.ResponseStatusCode].TestCase = d.TestCase
|
||||||
|
negUnresolved[payload][d.ResponseStatusCode].Encoders[d.Encoder] = nil
|
||||||
|
negUnresolved[payload][d.ResponseStatusCode].Placeholders[d.Placeholder] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data.TruePositiveTests.Bypassed = negBypassed
|
||||||
|
data.TruePositiveTests.Unresolved = negUnresolved
|
||||||
|
data.TruePositiveTests.Failed = s.TruePositiveTests.Failed
|
||||||
|
|
||||||
|
// map[payload]map[statusCode]*testDetails
|
||||||
|
posBlocked := make(map[string]map[int]*report.TestDetails)
|
||||||
|
for _, d := range s.TrueNegativeTests.Blocked {
|
||||||
|
payload := truncatePayload(d.Payload)
|
||||||
|
|
||||||
|
if _, ok := posBlocked[payload]; !ok {
|
||||||
|
// map[statusCode]*testDetails
|
||||||
|
posBlocked[payload] = make(map[int]*report.TestDetails)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := posBlocked[payload][d.ResponseStatusCode]; !ok {
|
||||||
|
posBlocked[payload][d.ResponseStatusCode] = &report.TestDetails{
|
||||||
|
Encoders: make(map[string]any),
|
||||||
|
Placeholders: make(map[string]any),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
posBlocked[payload][d.ResponseStatusCode].TestCase = d.TestCase
|
||||||
|
posBlocked[payload][d.ResponseStatusCode].Encoders[d.Encoder] = nil
|
||||||
|
posBlocked[payload][d.ResponseStatusCode].Placeholders[d.Placeholder] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// map[payload]map[statusCode]*testDetails
|
||||||
|
posBypassed := make(map[string]map[int]*report.TestDetails)
|
||||||
|
for _, d := range s.TrueNegativeTests.Bypasses {
|
||||||
|
payload := truncatePayload(d.Payload)
|
||||||
|
|
||||||
|
if _, ok := posBypassed[payload]; !ok {
|
||||||
|
// map[statusCode]*testDetails
|
||||||
|
posBypassed[payload] = make(map[int]*report.TestDetails)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := posBypassed[payload][d.ResponseStatusCode]; !ok {
|
||||||
|
posBypassed[payload][d.ResponseStatusCode] = &report.TestDetails{
|
||||||
|
Encoders: make(map[string]any),
|
||||||
|
Placeholders: make(map[string]any),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
posBypassed[payload][d.ResponseStatusCode].TestCase = d.TestCase
|
||||||
|
posBypassed[payload][d.ResponseStatusCode].Encoders[d.Encoder] = nil
|
||||||
|
posBypassed[payload][d.ResponseStatusCode].Placeholders[d.Placeholder] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// map[payload]map[statusCode]*testDetails
|
||||||
|
posUnresolved := make(map[string]map[int]*report.TestDetails)
|
||||||
|
for _, d := range s.TrueNegativeTests.Unresolved {
|
||||||
|
payload := truncatePayload(d.Payload)
|
||||||
|
|
||||||
|
if _, ok := posUnresolved[payload]; !ok {
|
||||||
|
// map[statusCode]*testDetails
|
||||||
|
posUnresolved[payload] = make(map[int]*report.TestDetails)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := posUnresolved[payload][d.ResponseStatusCode]; !ok {
|
||||||
|
posUnresolved[payload][d.ResponseStatusCode] = &report.TestDetails{
|
||||||
|
Encoders: make(map[string]any),
|
||||||
|
Placeholders: make(map[string]any),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
posUnresolved[payload][d.ResponseStatusCode].TestCase = d.TestCase
|
||||||
|
posUnresolved[payload][d.ResponseStatusCode].Encoders[d.Encoder] = nil
|
||||||
|
posUnresolved[payload][d.ResponseStatusCode].Placeholders[d.Placeholder] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data.TrueNegativeTests.Blocked = posBlocked
|
||||||
|
data.TrueNegativeTests.Bypassed = posBypassed
|
||||||
|
data.TrueNegativeTests.Unresolved = posUnresolved
|
||||||
|
data.TrueNegativeTests.Failed = s.TrueNegativeTests.Failed
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ScannedPaths = s.Paths
|
||||||
|
|
||||||
|
data.TruePositiveTests.Percentage = s.TruePositiveTests.ResolvedBlockedRequestsPercentage
|
||||||
|
data.TruePositiveTests.TotalSent = s.TruePositiveTests.ReqStats.AllRequestsNumber
|
||||||
|
data.TruePositiveTests.BlockedRequestsNumber = s.TruePositiveTests.ReqStats.BlockedRequestsNumber
|
||||||
|
data.TruePositiveTests.BypassedRequestsNumber = s.TruePositiveTests.ReqStats.BypassedRequestsNumber
|
||||||
|
data.TruePositiveTests.UnresolvedRequestsNumber = s.TruePositiveTests.ReqStats.UnresolvedRequestsNumber
|
||||||
|
data.TruePositiveTests.FailedRequestsNumber = s.TruePositiveTests.ReqStats.FailedRequestsNumber
|
||||||
|
|
||||||
|
data.TrueNegativeTests.Percentage = s.TrueNegativeTests.ResolvedBypassedRequestsPercentage
|
||||||
|
data.TrueNegativeTests.TotalSent = s.TrueNegativeTests.ReqStats.AllRequestsNumber
|
||||||
|
data.TrueNegativeTests.BlockedRequestsNumber = s.TrueNegativeTests.ReqStats.BlockedRequestsNumber
|
||||||
|
data.TrueNegativeTests.BypassedRequestsNumber = s.TrueNegativeTests.ReqStats.BypassedRequestsNumber
|
||||||
|
data.TrueNegativeTests.UnresolvedRequestsNumber = s.TrueNegativeTests.ReqStats.UnresolvedRequestsNumber
|
||||||
|
data.TrueNegativeTests.FailedRequestsNumber = s.TrueNegativeTests.ReqStats.FailedRequestsNumber
|
||||||
|
|
||||||
|
data.TotalSent = data.TruePositiveTests.TotalSent + data.TrueNegativeTests.TotalSent
|
||||||
|
data.BlockedRequestsNumber = data.TruePositiveTests.BlockedRequestsNumber + data.TrueNegativeTests.BlockedRequestsNumber
|
||||||
|
data.BypassedRequestsNumber = data.TruePositiveTests.BypassedRequestsNumber + data.TrueNegativeTests.BypassedRequestsNumber
|
||||||
|
data.UnresolvedRequestsNumber = data.TruePositiveTests.UnresolvedRequestsNumber + data.TrueNegativeTests.UnresolvedRequestsNumber
|
||||||
|
data.FailedRequestsNumber = data.TruePositiveTests.FailedRequestsNumber + data.TrueNegativeTests.FailedRequestsNumber
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// oncePrepareHTMLFullReport prepares ready data to insert into the HTML template
|
||||||
|
// once at the first call, and then reuses the previously prepared data
|
||||||
|
func oncePrepareHTMLFullReport(
|
||||||
|
s *db.Statistics, reportTime time.Time, wafName string,
|
||||||
|
url string, openApiFile string, args []string, ignoreUnresolved bool, includePayloads bool,
|
||||||
|
) (*report.HtmlReport, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
prepareHTMLReportOnce.Do(func() {
|
||||||
|
htmlReportData, err = prepareHTMLFullReport(
|
||||||
|
s, reportTime, wafName, url, openApiFile,
|
||||||
|
args, ignoreUnresolved, includePayloads,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return htmlReportData, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// exportFullReportToHtml prepares and saves a full report in HTML format on a disk
|
||||||
|
// to a temporary file.
|
||||||
|
func exportFullReportToHtml(
|
||||||
|
s *db.Statistics, reportTime time.Time, wafName string,
|
||||||
|
url string, openApiFile string, args []string, ignoreUnresolved bool, includePayloads bool,
|
||||||
|
) (fileName string, err error) {
|
||||||
|
reportData, err := oncePrepareHTMLFullReport(s, reportTime, wafName, url, openApiFile, args, ignoreUnresolved, includePayloads)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "couldn't prepare data for HTML report")
|
||||||
|
}
|
||||||
|
|
||||||
|
reportHtml, err := report.RenderFullReportToHTML(reportData)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "couldn't substitute report data into HTML template")
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.CreateTemp("", "gotestwaf_report_*.html")
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "couldn't create a temporary file")
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
fileName = file.Name()
|
||||||
|
|
||||||
|
file.Write(reportHtml.Bytes())
|
||||||
|
|
||||||
|
err = os.Chmod(fileName, 0644)
|
||||||
|
|
||||||
|
return fileName, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// printFullReportToHtml prepares and saves a full report in HTML format on a disk.
|
||||||
|
func printFullReportToHtml(
|
||||||
|
s *db.Statistics, reportFile string, reportTime time.Time,
|
||||||
|
wafName string, url string, openApiFile string, args []string,
|
||||||
|
ignoreUnresolved bool, includePayloads bool,
|
||||||
|
) error {
|
||||||
|
tempFileName, err := exportFullReportToHtml(s, reportTime, wafName, url, openApiFile, args, ignoreUnresolved, includePayloads)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't export report to HTML")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = helpers.FileMove(tempFileName, reportFile)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't export report to HTML")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
304
AttackSurface/src/gotestwaf/internal/report/json.go
Normal file
304
AttackSurface/src/gotestwaf/internal/report/json.go
Normal file
|
|
@ -0,0 +1,304 @@
|
||||||
|
package report
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// jsonReport represents a data required to render a full report in JSON format.
|
||||||
|
type jsonReport struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
ProjectName string `json:"project_name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Score float64 `json:"score,omitempty"`
|
||||||
|
TestCasesFP string `json:"fp"`
|
||||||
|
Args string `json:"args"`
|
||||||
|
|
||||||
|
// fields for console report in JSON format
|
||||||
|
TruePositiveTests *testsInfo `json:"true_positive_tests,omitempty"`
|
||||||
|
TrueNegativeTests *testsInfo `json:"true_negative_tests,omitempty"`
|
||||||
|
|
||||||
|
// fields for full report in JSON format
|
||||||
|
Summary *summary `json:"summary,omitempty"`
|
||||||
|
TruePositiveTestsPayloads *testPayloads `json:"true_positive_tests_payloads,omitempty"`
|
||||||
|
TrueNegativeTestsPayloads *testPayloads `json:"true_negative_tests_payloads,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type testsInfo struct {
|
||||||
|
Score float64 `json:"score"`
|
||||||
|
|
||||||
|
Summary requestStats `json:"summary"`
|
||||||
|
ApiSecStat requestStats `json:"api_sec"`
|
||||||
|
AppSecStat requestStats `json:"app_sec"`
|
||||||
|
|
||||||
|
TestSets testSets `json:"test_sets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type requestStats struct {
|
||||||
|
TotalSent int `json:"total_sent"`
|
||||||
|
ResolvedTests int `json:"resolved_tests"`
|
||||||
|
BlockedTests int `json:"blocked_tests"`
|
||||||
|
BypassedTests int `json:"bypassed_tests"`
|
||||||
|
UnresolvedTests int `json:"unresolved_tests"`
|
||||||
|
FailedTests int `json:"failed_tests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type testSets map[string]testCases
|
||||||
|
|
||||||
|
type testCases map[string]*testCaseInfo
|
||||||
|
|
||||||
|
type testCaseInfo struct {
|
||||||
|
Percentage float64 `json:"percentage"`
|
||||||
|
Sent int `json:"sent"`
|
||||||
|
Blocked int `json:"blocked"`
|
||||||
|
Bypassed int `json:"bypassed"`
|
||||||
|
Unresolved int `json:"unresolved"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type summary struct {
|
||||||
|
TruePositiveTests *testsInfo `json:"true_positive_tests,omitempty"`
|
||||||
|
TrueNegativeTests *testsInfo `json:"true_negative_tests,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type testPayloads struct {
|
||||||
|
Blocked []*payloadDetails `json:"blocked,omitempty"`
|
||||||
|
Bypassed []*payloadDetails `json:"bypassed,omitempty"`
|
||||||
|
Unresolved []*payloadDetails `json:"unresolved,omitempty"`
|
||||||
|
Failed []*payloadDetails `json:"failed,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type payloadDetails struct {
|
||||||
|
Payload string `json:"payload"`
|
||||||
|
TestSet string `json:"test_set"`
|
||||||
|
TestCase string `json:"test_case"`
|
||||||
|
Encoder string `json:"encoder"`
|
||||||
|
Placeholder string `json:"placeholder"`
|
||||||
|
Status int `json:"status,omitempty"`
|
||||||
|
TestResult string `json:"test_result"`
|
||||||
|
|
||||||
|
// Used for non-failed payloads
|
||||||
|
AdditionalInformation []string `json:"additional_info,omitempty"`
|
||||||
|
|
||||||
|
// Used for failed payloads
|
||||||
|
Reason []string `json:"reason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// printFullReportToJson prepares and prints a full report in JSON format to the file.
|
||||||
|
func printFullReportToJson(
|
||||||
|
s *db.Statistics, reportFile string, reportTime time.Time,
|
||||||
|
wafName string, url string, args []string, ignoreUnresolved bool,
|
||||||
|
) error {
|
||||||
|
report := jsonReport{
|
||||||
|
Date: reportTime.Format(time.ANSIC),
|
||||||
|
ProjectName: wafName,
|
||||||
|
URL: url,
|
||||||
|
Score: s.Score.Average,
|
||||||
|
TestCasesFP: s.TestCasesFingerprint,
|
||||||
|
Args: strings.Join(args, " "),
|
||||||
|
}
|
||||||
|
|
||||||
|
report.Summary = &summary{}
|
||||||
|
|
||||||
|
if len(s.TruePositiveTests.SummaryTable) != 0 {
|
||||||
|
report.Summary.TruePositiveTests = &testsInfo{
|
||||||
|
Score: s.TruePositiveTests.ResolvedBlockedRequestsPercentage,
|
||||||
|
Summary: requestStats{
|
||||||
|
TotalSent: s.TruePositiveTests.ReqStats.AllRequestsNumber,
|
||||||
|
ResolvedTests: s.TruePositiveTests.ReqStats.ResolvedRequestsNumber,
|
||||||
|
BlockedTests: s.TruePositiveTests.ReqStats.BlockedRequestsNumber,
|
||||||
|
BypassedTests: s.TruePositiveTests.ReqStats.BypassedRequestsNumber,
|
||||||
|
UnresolvedTests: s.TruePositiveTests.ReqStats.UnresolvedRequestsNumber,
|
||||||
|
FailedTests: s.TruePositiveTests.ReqStats.FailedRequestsNumber,
|
||||||
|
},
|
||||||
|
ApiSecStat: requestStats{
|
||||||
|
TotalSent: s.TruePositiveTests.ApiSecReqStats.AllRequestsNumber,
|
||||||
|
ResolvedTests: s.TruePositiveTests.ApiSecReqStats.ResolvedRequestsNumber,
|
||||||
|
BlockedTests: s.TruePositiveTests.ApiSecReqStats.BlockedRequestsNumber,
|
||||||
|
BypassedTests: s.TruePositiveTests.ApiSecReqStats.BypassedRequestsNumber,
|
||||||
|
UnresolvedTests: s.TruePositiveTests.ApiSecReqStats.UnresolvedRequestsNumber,
|
||||||
|
FailedTests: s.TruePositiveTests.ApiSecReqStats.FailedRequestsNumber,
|
||||||
|
},
|
||||||
|
AppSecStat: requestStats{
|
||||||
|
TotalSent: s.TruePositiveTests.AppSecReqStats.AllRequestsNumber,
|
||||||
|
ResolvedTests: s.TruePositiveTests.AppSecReqStats.ResolvedRequestsNumber,
|
||||||
|
BlockedTests: s.TruePositiveTests.AppSecReqStats.BlockedRequestsNumber,
|
||||||
|
BypassedTests: s.TruePositiveTests.AppSecReqStats.BypassedRequestsNumber,
|
||||||
|
UnresolvedTests: s.TruePositiveTests.AppSecReqStats.UnresolvedRequestsNumber,
|
||||||
|
FailedTests: s.TruePositiveTests.AppSecReqStats.FailedRequestsNumber,
|
||||||
|
},
|
||||||
|
TestSets: make(testSets),
|
||||||
|
}
|
||||||
|
for _, row := range s.TruePositiveTests.SummaryTable {
|
||||||
|
if report.Summary.TruePositiveTests.TestSets[row.TestSet] == nil {
|
||||||
|
report.Summary.TruePositiveTests.TestSets[row.TestSet] = make(testCases)
|
||||||
|
}
|
||||||
|
report.Summary.TruePositiveTests.TestSets[row.TestSet][row.TestCase] = &testCaseInfo{
|
||||||
|
Percentage: row.Percentage,
|
||||||
|
Sent: row.Sent,
|
||||||
|
Blocked: row.Blocked,
|
||||||
|
Bypassed: row.Bypassed,
|
||||||
|
Unresolved: row.Unresolved,
|
||||||
|
Failed: row.Failed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.TrueNegativeTests.SummaryTable) != 0 {
|
||||||
|
report.Summary.TrueNegativeTests = &testsInfo{
|
||||||
|
Score: s.TrueNegativeTests.ResolvedBypassedRequestsPercentage,
|
||||||
|
Summary: requestStats{
|
||||||
|
TotalSent: s.TrueNegativeTests.ReqStats.AllRequestsNumber,
|
||||||
|
ResolvedTests: s.TrueNegativeTests.ReqStats.ResolvedRequestsNumber,
|
||||||
|
BlockedTests: s.TrueNegativeTests.ReqStats.BlockedRequestsNumber,
|
||||||
|
BypassedTests: s.TrueNegativeTests.ReqStats.BypassedRequestsNumber,
|
||||||
|
UnresolvedTests: s.TrueNegativeTests.ReqStats.UnresolvedRequestsNumber,
|
||||||
|
FailedTests: s.TrueNegativeTests.ReqStats.FailedRequestsNumber,
|
||||||
|
},
|
||||||
|
ApiSecStat: requestStats{
|
||||||
|
TotalSent: s.TrueNegativeTests.ApiSecReqStats.AllRequestsNumber,
|
||||||
|
ResolvedTests: s.TrueNegativeTests.ApiSecReqStats.ResolvedRequestsNumber,
|
||||||
|
BlockedTests: s.TrueNegativeTests.ApiSecReqStats.BlockedRequestsNumber,
|
||||||
|
BypassedTests: s.TrueNegativeTests.ApiSecReqStats.BypassedRequestsNumber,
|
||||||
|
UnresolvedTests: s.TrueNegativeTests.ApiSecReqStats.UnresolvedRequestsNumber,
|
||||||
|
FailedTests: s.TrueNegativeTests.ApiSecReqStats.FailedRequestsNumber,
|
||||||
|
},
|
||||||
|
AppSecStat: requestStats{
|
||||||
|
TotalSent: s.TrueNegativeTests.AppSecReqStats.AllRequestsNumber,
|
||||||
|
ResolvedTests: s.TrueNegativeTests.AppSecReqStats.ResolvedRequestsNumber,
|
||||||
|
BlockedTests: s.TrueNegativeTests.AppSecReqStats.BlockedRequestsNumber,
|
||||||
|
BypassedTests: s.TrueNegativeTests.AppSecReqStats.BypassedRequestsNumber,
|
||||||
|
UnresolvedTests: s.TrueNegativeTests.AppSecReqStats.UnresolvedRequestsNumber,
|
||||||
|
FailedTests: s.TrueNegativeTests.AppSecReqStats.FailedRequestsNumber,
|
||||||
|
},
|
||||||
|
TestSets: make(testSets),
|
||||||
|
}
|
||||||
|
for _, row := range s.TrueNegativeTests.SummaryTable {
|
||||||
|
if report.Summary.TrueNegativeTests.TestSets[row.TestSet] == nil {
|
||||||
|
report.Summary.TrueNegativeTests.TestSets[row.TestSet] = make(testCases)
|
||||||
|
}
|
||||||
|
report.Summary.TrueNegativeTests.TestSets[row.TestSet][row.TestCase] = &testCaseInfo{
|
||||||
|
Percentage: row.Percentage,
|
||||||
|
Sent: row.Sent,
|
||||||
|
Blocked: row.Blocked,
|
||||||
|
Bypassed: row.Bypassed,
|
||||||
|
Unresolved: row.Unresolved,
|
||||||
|
Failed: row.Failed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
report.TruePositiveTestsPayloads = &testPayloads{}
|
||||||
|
|
||||||
|
for _, bypass := range s.TruePositiveTests.Bypasses {
|
||||||
|
bypassDetail := &payloadDetails{
|
||||||
|
Payload: bypass.Payload,
|
||||||
|
TestSet: bypass.TestSet,
|
||||||
|
TestCase: bypass.TestCase,
|
||||||
|
Encoder: bypass.Encoder,
|
||||||
|
Placeholder: bypass.Encoder,
|
||||||
|
Status: bypass.ResponseStatusCode,
|
||||||
|
TestResult: "failed",
|
||||||
|
AdditionalInformation: bypass.AdditionalInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
report.TruePositiveTestsPayloads.Bypassed = append(report.TruePositiveTestsPayloads.Bypassed, bypassDetail)
|
||||||
|
}
|
||||||
|
if !ignoreUnresolved {
|
||||||
|
for _, unresolved := range s.TruePositiveTests.Unresolved {
|
||||||
|
unresolvedDetail := &payloadDetails{
|
||||||
|
Payload: unresolved.Payload,
|
||||||
|
TestSet: unresolved.TestSet,
|
||||||
|
TestCase: unresolved.TestCase,
|
||||||
|
Encoder: unresolved.Encoder,
|
||||||
|
Placeholder: unresolved.Encoder,
|
||||||
|
Status: unresolved.ResponseStatusCode,
|
||||||
|
TestResult: "unknown",
|
||||||
|
AdditionalInformation: unresolved.AdditionalInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
report.TruePositiveTestsPayloads.Unresolved = append(report.TruePositiveTestsPayloads.Unresolved, unresolvedDetail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, failed := range s.TruePositiveTests.Failed {
|
||||||
|
failedDetail := &payloadDetails{
|
||||||
|
Payload: failed.Payload,
|
||||||
|
TestSet: failed.TestSet,
|
||||||
|
TestCase: failed.TestCase,
|
||||||
|
Encoder: failed.Encoder,
|
||||||
|
Placeholder: failed.Encoder,
|
||||||
|
Reason: failed.Reason,
|
||||||
|
}
|
||||||
|
|
||||||
|
report.TruePositiveTestsPayloads.Failed = append(report.TruePositiveTestsPayloads.Failed, failedDetail)
|
||||||
|
}
|
||||||
|
|
||||||
|
report.TrueNegativeTestsPayloads = &testPayloads{}
|
||||||
|
|
||||||
|
for _, blocked := range s.TrueNegativeTests.Blocked {
|
||||||
|
blockedDetails := &payloadDetails{
|
||||||
|
Payload: blocked.Payload,
|
||||||
|
TestSet: blocked.TestSet,
|
||||||
|
TestCase: blocked.TestCase,
|
||||||
|
Encoder: blocked.Encoder,
|
||||||
|
Placeholder: blocked.Encoder,
|
||||||
|
Status: blocked.ResponseStatusCode,
|
||||||
|
TestResult: "failed",
|
||||||
|
AdditionalInformation: blocked.AdditionalInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
report.TrueNegativeTestsPayloads.Blocked = append(report.TrueNegativeTestsPayloads.Blocked, blockedDetails)
|
||||||
|
}
|
||||||
|
if !ignoreUnresolved {
|
||||||
|
for _, unresolved := range s.TrueNegativeTests.Unresolved {
|
||||||
|
unresolvedDetail := &payloadDetails{
|
||||||
|
Payload: unresolved.Payload,
|
||||||
|
TestSet: unresolved.TestSet,
|
||||||
|
TestCase: unresolved.TestCase,
|
||||||
|
Encoder: unresolved.Encoder,
|
||||||
|
Placeholder: unresolved.Encoder,
|
||||||
|
Status: unresolved.ResponseStatusCode,
|
||||||
|
TestResult: "unknown",
|
||||||
|
AdditionalInformation: unresolved.AdditionalInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
report.TrueNegativeTestsPayloads.Unresolved = append(report.TrueNegativeTestsPayloads.Unresolved, unresolvedDetail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, failed := range s.TrueNegativeTests.Failed {
|
||||||
|
failedDetail := &payloadDetails{
|
||||||
|
Payload: failed.Payload,
|
||||||
|
TestSet: failed.TestSet,
|
||||||
|
TestCase: failed.TestCase,
|
||||||
|
Encoder: failed.Encoder,
|
||||||
|
Placeholder: failed.Encoder,
|
||||||
|
Reason: failed.Reason,
|
||||||
|
}
|
||||||
|
|
||||||
|
report.TrueNegativeTestsPayloads.Failed = append(report.TrueNegativeTestsPayloads.Failed, failedDetail)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBytes, err := json.MarshalIndent(report, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't dump report to JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(reportFile)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't create file")
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = file.Write(jsonBytes)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't write report to file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
30
AttackSurface/src/gotestwaf/internal/report/pdf.go
Normal file
30
AttackSurface/src/gotestwaf/internal/report/pdf.go
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
package report
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
func printFullReportToPdf(
|
||||||
|
ctx context.Context, s *db.Statistics, reportFile string, reportTime time.Time,
|
||||||
|
wafName string, url string, openApiFile string, args []string, ignoreUnresolved bool,
|
||||||
|
includePayloads bool,
|
||||||
|
) error {
|
||||||
|
tempFileName, err := exportFullReportToHtml(s, reportTime, wafName, url, openApiFile, args, ignoreUnresolved, includePayloads)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't export report to HTML")
|
||||||
|
}
|
||||||
|
|
||||||
|
tempFileURL := "file://" + tempFileName
|
||||||
|
|
||||||
|
err = renderToPDF(ctx, tempFileURL, reportFile)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't render HTML report to PDF")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
61
AttackSurface/src/gotestwaf/internal/report/render.go
Normal file
61
AttackSurface/src/gotestwaf/internal/report/render.go
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
package report
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/chromedp/cdproto/page"
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// chromium-browser \
|
||||||
|
// --headless \
|
||||||
|
// --no-zygote \
|
||||||
|
// --single-process \
|
||||||
|
// --no-sandbox \
|
||||||
|
// --disable-gpu \
|
||||||
|
// --run-all-compositor-stages-before-draw \
|
||||||
|
// --no-pdf-header-footer \
|
||||||
|
// --print-to-pdf=test.pdf \
|
||||||
|
// report.html
|
||||||
|
|
||||||
|
var chromeDPExecAllocatorOptions = append(
|
||||||
|
chromedp.DefaultExecAllocatorOptions[:],
|
||||||
|
chromedp.Flag("no-zygote", true),
|
||||||
|
chromedp.Flag("no-sandbox", true),
|
||||||
|
chromedp.Flag("disable-gpu", true),
|
||||||
|
chromedp.Flag("run-all-compositor-stages-before-draw", true),
|
||||||
|
)
|
||||||
|
|
||||||
|
func renderToPDF(ctx context.Context, fileToRenderURL string, pathToResultPDF string) error {
|
||||||
|
allocCtx, allocCtxCancel := chromedp.NewExecAllocator(ctx, chromeDPExecAllocatorOptions...)
|
||||||
|
defer allocCtxCancel()
|
||||||
|
|
||||||
|
chromeCtx, chromeCtxCancel := chromedp.NewContext(allocCtx)
|
||||||
|
defer chromeCtxCancel()
|
||||||
|
|
||||||
|
var buf []byte
|
||||||
|
|
||||||
|
tasks := chromedp.Tasks{
|
||||||
|
chromedp.Navigate(fileToRenderURL),
|
||||||
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
|
var err error
|
||||||
|
buf, _, err = page.PrintToPDF().WithPrintBackground(true).Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := chromedp.Run(chromeCtx, tasks); err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't render HTML file to PDF")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(pathToResultPDF, buf, 0o644); err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't save PDF file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
159
AttackSurface/src/gotestwaf/internal/report/report.go
Normal file
159
AttackSurface/src/gotestwaf/internal/report/report.go
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
package report
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxReportFilenameLength = 249 // 255 (max length) - 5 (".html") - 1 (to be sure)
|
||||||
|
|
||||||
|
consoleReportTextFormat = "text"
|
||||||
|
consoleReportJsonFormat = "json"
|
||||||
|
)
|
||||||
|
const (
|
||||||
|
NoneFormat = "none"
|
||||||
|
JsonFormat = "json"
|
||||||
|
HtmlFormat = "html"
|
||||||
|
PdfFormat = "pdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ReportFormatsSet = map[string]any{
|
||||||
|
NoneFormat: nil,
|
||||||
|
JsonFormat: nil,
|
||||||
|
HtmlFormat: nil,
|
||||||
|
PdfFormat: nil,
|
||||||
|
}
|
||||||
|
ReportFormats = slices.Collect(maps.Keys(ReportFormatsSet))
|
||||||
|
)
|
||||||
|
|
||||||
|
func SendReportByEmail(
|
||||||
|
ctx context.Context, s *db.Statistics, email string, reportTime time.Time,
|
||||||
|
wafName string, url string, openApiFile string, args []string, ignoreUnresolved bool, includePayloads bool,
|
||||||
|
) error {
|
||||||
|
reportData, err := oncePrepareHTMLFullReport(s, reportTime, wafName, url, openApiFile, args, ignoreUnresolved, includePayloads)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't prepare data for HTML report")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sendEmail(ctx, reportData, email)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportFullReport saves full report on disk in different formats: HTML, PDF, JSON.
|
||||||
|
func ExportFullReport(
|
||||||
|
ctx context.Context, s *db.Statistics, reportFile string, reportTime time.Time,
|
||||||
|
wafName string, url string, openApiFile string, args []string, ignoreUnresolved bool,
|
||||||
|
includePayloads bool, formats []string,
|
||||||
|
) (reportFileNames []string, err error) {
|
||||||
|
_, reportFileName := filepath.Split(reportFile)
|
||||||
|
if len(reportFileName) > maxReportFilenameLength {
|
||||||
|
return nil, errors.New("report filename too long")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, format := range formats {
|
||||||
|
switch format {
|
||||||
|
case HtmlFormat:
|
||||||
|
reportFileName = reportFile + ".html"
|
||||||
|
err = printFullReportToHtml(s, reportFileName, reportTime, wafName, url, openApiFile, args, ignoreUnresolved, includePayloads)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
case PdfFormat:
|
||||||
|
reportFileName = reportFile + ".pdf"
|
||||||
|
err = printFullReportToPdf(ctx, s, reportFileName, reportTime, wafName, url, openApiFile, args, ignoreUnresolved, includePayloads)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
case JsonFormat:
|
||||||
|
reportFileName = reportFile + ".json"
|
||||||
|
err = printFullReportToJson(s, reportFileName, reportTime, wafName, url, args, ignoreUnresolved)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
case NoneFormat:
|
||||||
|
return nil, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown report format: %s", format)
|
||||||
|
}
|
||||||
|
|
||||||
|
reportFileNames = append(reportFileNames, reportFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return reportFileNames, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateReportFormat(formats []string) error {
|
||||||
|
if len(formats) == 0 {
|
||||||
|
return errors.New("no report format specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert slice to set (map)
|
||||||
|
set := make(map[string]any)
|
||||||
|
for _, s := range formats {
|
||||||
|
if _, ok := ReportFormatsSet[s]; !ok {
|
||||||
|
return fmt.Errorf("unknown report format: %s", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
set[s] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicating values
|
||||||
|
if len(set) != len(formats) {
|
||||||
|
return fmt.Errorf("found duplicated values: %s", strings.Join(formats, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check "none" is present
|
||||||
|
_, isNone := set[NoneFormat]
|
||||||
|
|
||||||
|
// Check for conflicts
|
||||||
|
if len(set) > 1 && isNone {
|
||||||
|
// Delete "none" from the set
|
||||||
|
delete(set, NoneFormat)
|
||||||
|
// Collect conflicted formats
|
||||||
|
conflictedFormats := slices.Collect(maps.Keys(set))
|
||||||
|
|
||||||
|
return fmt.Errorf("\"none\" conflicts with other formats: %s", strings.Join(conflictedFormats, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsNoneReportFormat(reportFormat []string) bool {
|
||||||
|
if len(reportFormat) > 0 && reportFormat[0] == NoneFormat {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsPdfOrHtmlReportFormat(reportFormats []string) bool {
|
||||||
|
for _, format := range reportFormats {
|
||||||
|
if format == PdfFormat {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if format == HtmlFormat {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,320 @@
|
||||||
|
package chrome
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/chromedp/cdproto/cdp"
|
||||||
|
"github.com/chromedp/cdproto/network"
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/config"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/helpers"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/payload"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/clients"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ clients.HTTPClient = (*Client)(nil)
|
||||||
|
|
||||||
|
var DefaultChromeDPExecAllocatorOptions = append(
|
||||||
|
chromedp.DefaultExecAllocatorOptions[:],
|
||||||
|
// Disable the CORS policy constraints
|
||||||
|
chromedp.Flag("disable-web-security", true),
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
execAllocatorOptions []chromedp.ExecAllocatorOption
|
||||||
|
disableLogs bool
|
||||||
|
|
||||||
|
headers map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(cfg *config.Config) (*Client, error) {
|
||||||
|
execAllocatorOptions := DefaultChromeDPExecAllocatorOptions[:]
|
||||||
|
|
||||||
|
disableLogs := false
|
||||||
|
logLevel, _ := logrus.ParseLevel(cfg.LogLevel)
|
||||||
|
if logLevel < logrus.DebugLevel {
|
||||||
|
disableLogs = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Proxy != "" {
|
||||||
|
execAllocatorOptions = append(
|
||||||
|
execAllocatorOptions,
|
||||||
|
chromedp.ProxyServer(cfg.Proxy),
|
||||||
|
// By default, Chrome will bypass localhost.
|
||||||
|
// The test server is bound to localhost, so we should add the
|
||||||
|
// following flag to use the proxy for localhost URLs.
|
||||||
|
chromedp.Flag("proxy-bypass-list", "<-loopback>"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.TLSVerify {
|
||||||
|
execAllocatorOptions = append(
|
||||||
|
execAllocatorOptions,
|
||||||
|
chromedp.Flag("ignore-certificate-errors", "1"),
|
||||||
|
chromedp.Flag("allow-insecure-localhost", "1"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
configuredHeaders := helpers.DeepCopyMap(cfg.HTTPHeaders)
|
||||||
|
for k := range configuredHeaders {
|
||||||
|
if strings.EqualFold(k, "host") {
|
||||||
|
delete(configuredHeaders, k)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customHeader := strings.SplitN(cfg.AddHeader, ":", 2)
|
||||||
|
if len(customHeader) > 1 {
|
||||||
|
header := strings.TrimSpace(customHeader[0])
|
||||||
|
value := strings.TrimSpace(customHeader[1])
|
||||||
|
configuredHeaders[header] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Client{
|
||||||
|
execAllocatorOptions: execAllocatorOptions,
|
||||||
|
disableLogs: disableLogs,
|
||||||
|
headers: configuredHeaders,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SendPayload(
|
||||||
|
ctx context.Context,
|
||||||
|
targetURL string,
|
||||||
|
payloadInfo *payload.PayloadInfo,
|
||||||
|
) (types.Response, error) {
|
||||||
|
request, err := payloadInfo.GetRequest(targetURL, types.ChromeHTTPClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "couldn't prepare request")
|
||||||
|
}
|
||||||
|
|
||||||
|
r, ok := request.(*types.ChromeDPTasks)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("bad request type: %T, expected %T", request, &types.ChromeDPTasks{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new Chrome allocator context
|
||||||
|
allocCtx, allocCtxCancel := chromedp.NewExecAllocator(ctx, c.execAllocatorOptions...)
|
||||||
|
defer allocCtxCancel()
|
||||||
|
|
||||||
|
var logOptions []chromedp.ContextOption
|
||||||
|
if c.disableLogs {
|
||||||
|
logOptions = append(
|
||||||
|
logOptions,
|
||||||
|
chromedp.WithLogf(discardLogs),
|
||||||
|
chromedp.WithDebugf(discardLogs),
|
||||||
|
chromedp.WithErrorf(discardLogs),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new Chrome context
|
||||||
|
chromeCtx, chromeCtxCancel := chromedp.NewContext(allocCtx, logOptions...)
|
||||||
|
defer chromeCtxCancel()
|
||||||
|
|
||||||
|
headers := make(network.Headers)
|
||||||
|
for k, v := range c.headers {
|
||||||
|
if strings.EqualFold(k, "host") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
headers[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
errorChan := make(chan error, 10)
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
// Get home page
|
||||||
|
tasks := chromedp.Tasks{chromedp.Navigate(targetURL)}
|
||||||
|
if len(headers) > 0 {
|
||||||
|
tasks = append(chromedp.Tasks{network.SetExtraHTTPHeaders(headers)}, tasks...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := chromedp.Run(chromeCtx, tasks); err != nil {
|
||||||
|
errorChan <- errors.Wrap(err, "failed to execute Chrome tasks")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform request with payload
|
||||||
|
if payloadInfo.DebugHeaderValue != "" {
|
||||||
|
headers[clients.GTWDebugHeader] = payloadInfo.DebugHeaderValue
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range r.UserAgentHeader {
|
||||||
|
headers[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks = chromedp.Tasks{}
|
||||||
|
if len(headers) > 0 {
|
||||||
|
tasks = chromedp.Tasks{network.SetExtraHTTPHeaders(headers)}
|
||||||
|
}
|
||||||
|
tasks = append(tasks, r.Tasks...)
|
||||||
|
|
||||||
|
if err := chromedp.Run(chromeCtx, tasks); err != nil {
|
||||||
|
errorChan <- errors.Wrap(err, "failed to execute Chrome tasks")
|
||||||
|
}
|
||||||
|
|
||||||
|
close(errorChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = nil
|
||||||
|
|
||||||
|
// Collect errors
|
||||||
|
forLoop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case e, ok := <-errorChan:
|
||||||
|
if !ok {
|
||||||
|
break forLoop
|
||||||
|
}
|
||||||
|
err = multierror.Append(err, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait Chrome-related goroutines
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.ResponseMeta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SendRequest(
|
||||||
|
ctx context.Context,
|
||||||
|
req types.Request,
|
||||||
|
) (types.Response, error) {
|
||||||
|
r, ok := req.(*types.ChromeDPTasks)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("bad request type: %T, expected %T", req, &types.ChromeDPTasks{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new Chrome allocator context
|
||||||
|
allocCtx, allocCtxCancel := chromedp.NewExecAllocator(ctx, c.execAllocatorOptions...)
|
||||||
|
defer allocCtxCancel()
|
||||||
|
|
||||||
|
var logOptions []chromedp.ContextOption
|
||||||
|
if c.disableLogs {
|
||||||
|
logOptions = append(
|
||||||
|
logOptions,
|
||||||
|
chromedp.WithLogf(discardLogs),
|
||||||
|
chromedp.WithDebugf(discardLogs),
|
||||||
|
chromedp.WithErrorf(discardLogs),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new Chrome context
|
||||||
|
chromeCtx, chromeCtxCancel := chromedp.NewContext(allocCtx, logOptions...)
|
||||||
|
defer chromeCtxCancel()
|
||||||
|
|
||||||
|
headers := make(network.Headers)
|
||||||
|
for k, v := range c.headers {
|
||||||
|
if strings.EqualFold(k, "host") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
headers[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.DebugHeaderValue != "" {
|
||||||
|
headers[clients.GTWDebugHeader] = r.DebugHeaderValue
|
||||||
|
}
|
||||||
|
|
||||||
|
var tasks chromedp.Tasks
|
||||||
|
if len(headers) > 0 {
|
||||||
|
tasks = append(chromedp.Tasks{network.SetExtraHTTPHeaders(headers)}, tasks...)
|
||||||
|
}
|
||||||
|
tasks = append(tasks, r.Tasks...)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
errorChan := make(chan error, 10)
|
||||||
|
|
||||||
|
// Hold the latest response information
|
||||||
|
var latestResponse *types.ResponseMeta
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
// Enable Network domain and set request interception
|
||||||
|
if err = chromedp.Run(chromeCtx, network.Enable()); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "couldn't enable network domain")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for network events
|
||||||
|
chromedp.ListenTarget(chromeCtx, func(ev interface{}) {
|
||||||
|
if ev, ok := ev.(*network.EventResponseReceived); ok {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
localCtx := chromedp.FromContext(chromeCtx)
|
||||||
|
executor := cdp.WithExecutor(chromeCtx, localCtx.Target)
|
||||||
|
|
||||||
|
// Get the response body
|
||||||
|
body, _ := network.GetResponseBody(ev.RequestID).Do(executor)
|
||||||
|
|
||||||
|
response := ev.Response
|
||||||
|
info := &types.ResponseMeta{
|
||||||
|
StatusCode: int(response.Status),
|
||||||
|
StatusReason: response.StatusText,
|
||||||
|
Headers: headersToMap(response.Headers),
|
||||||
|
Content: body,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the latest response
|
||||||
|
latestResponse = info
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
if err := chromedp.Run(chromeCtx, r.Tasks); err != nil {
|
||||||
|
errorChan <- errors.Wrap(err, "failed to execute Chrome tasks")
|
||||||
|
}
|
||||||
|
|
||||||
|
close(errorChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = nil
|
||||||
|
|
||||||
|
// Collect errors
|
||||||
|
forLoop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case e, ok := <-errorChan:
|
||||||
|
if !ok {
|
||||||
|
break forLoop
|
||||||
|
}
|
||||||
|
err = multierror.Append(err, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait Chrome-related goroutines
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return latestResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// discardLogs serves as a no-op logging function for chromedp
|
||||||
|
// to suppress all internal logging output.
|
||||||
|
func discardLogs(string, ...interface{}) {}
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/chromedp/cdproto/runtime"
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// jsCodeTemplate is a JavaScript code to send an HTTP request and
|
||||||
|
// return the response details.
|
||||||
|
const jsCodeTemplate = `
|
||||||
|
const f = async function() {
|
||||||
|
let err = null;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
'{{.URL}}',
|
||||||
|
{
|
||||||
|
method: "{{.Method}}",
|
||||||
|
{{if .Headers}}headers: {{.Headers}},{{end}}
|
||||||
|
{{if .Body}}body: {{.Body}}{{end}}
|
||||||
|
}
|
||||||
|
).catch(e => {
|
||||||
|
err = {
|
||||||
|
Error: e.message,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {};
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
headers[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const bodyBuffer = await response.arrayBuffer();
|
||||||
|
const body = Array.from(new Uint8Array(bodyBuffer));
|
||||||
|
|
||||||
|
return {
|
||||||
|
StatusCode: response.status,
|
||||||
|
StatusText: response.statusText,
|
||||||
|
Headers: headers,
|
||||||
|
Content: body
|
||||||
|
};
|
||||||
|
};
|
||||||
|
window.returnValue = f();
|
||||||
|
`
|
||||||
|
|
||||||
|
// RequestOptions represents fetch options.
|
||||||
|
type RequestOptions struct {
|
||||||
|
Method string
|
||||||
|
Headers map[string]string
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
URL string
|
||||||
|
Method string
|
||||||
|
Headers template.HTML
|
||||||
|
Body template.HTML
|
||||||
|
}
|
||||||
|
|
||||||
|
// response represents data received from JS script.
|
||||||
|
type response struct {
|
||||||
|
StatusCode int
|
||||||
|
StatusReason string
|
||||||
|
Headers map[string]string
|
||||||
|
Content []byte
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFetchRequest(targetURL string, reqOptions *RequestOptions) (chromedp.Action, *types.ResponseMeta, error) {
|
||||||
|
if reqOptions == nil {
|
||||||
|
return nil, nil, errors.New("no request options provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
if reqOptions.Method == "" {
|
||||||
|
return nil, nil, errors.New("request method is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &options{
|
||||||
|
URL: targetURL,
|
||||||
|
Method: reqOptions.Method,
|
||||||
|
}
|
||||||
|
|
||||||
|
if reqOptions.Headers != nil && len(reqOptions.Headers) > 0 {
|
||||||
|
headersJSON, err := json.Marshal(reqOptions.Headers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "failed to marshal headers")
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Headers = template.HTML(headersJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
if reqOptions.Body != "" {
|
||||||
|
opts.Body = template.HTML(reqOptions.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
t := template.Must(template.New("jsCodeTemplate").Parse(jsCodeTemplate))
|
||||||
|
|
||||||
|
jsCode := bytes.NewBuffer(nil)
|
||||||
|
if err := t.Execute(jsCode, opts); err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "couldn't create JS snippet")
|
||||||
|
}
|
||||||
|
|
||||||
|
responseMeta := &types.ResponseMeta{}
|
||||||
|
responseMetaRaw := &response{}
|
||||||
|
|
||||||
|
f := chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
|
// Run the JavaScript and capture the result
|
||||||
|
var jsResult []byte
|
||||||
|
err := chromedp.Evaluate(
|
||||||
|
jsCode.String(),
|
||||||
|
&jsResult,
|
||||||
|
func(p *runtime.EvaluateParams) *runtime.EvaluateParams {
|
||||||
|
return p.WithAwaitPromise(true)
|
||||||
|
},
|
||||||
|
).Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the JSON result into the provided result interface
|
||||||
|
err = json.Unmarshal(jsResult, responseMetaRaw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
responseMeta.Headers = make(http.Header)
|
||||||
|
responseMeta.StatusCode = responseMetaRaw.StatusCode
|
||||||
|
responseMeta.StatusReason = responseMetaRaw.StatusReason
|
||||||
|
responseMeta.Content = responseMetaRaw.Content
|
||||||
|
responseMeta.Error = responseMetaRaw.Error
|
||||||
|
|
||||||
|
for k, v := range responseMetaRaw.Headers {
|
||||||
|
responseMeta.Headers[k] = []string{v}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return f, responseMeta, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package chrome
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// headersToMap converts network.Headers (map[string]interface{}) to
|
||||||
|
// http.Header (map[string][]string)
|
||||||
|
func headersToMap(headers map[string]interface{}) http.Header {
|
||||||
|
result := make(http.Header)
|
||||||
|
|
||||||
|
for k, v := range headers {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case interface{}:
|
||||||
|
result.Add(k, fmt.Sprintf("%v", v))
|
||||||
|
case []interface{}:
|
||||||
|
for _, val := range v {
|
||||||
|
result.Add(k, fmt.Sprintf("%v", val))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result.Add(k, fmt.Sprintf("%v", v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package clients
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/payload"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const GTWDebugHeader = "X-GoTestWAF-Test"
|
||||||
|
|
||||||
|
// HTTPClient is an interface that defines methods for sending HTTP requests and payloads.
|
||||||
|
type HTTPClient interface {
|
||||||
|
// SendPayload sends a payload to the specified target URL.
|
||||||
|
SendPayload(ctx context.Context, targetURL string, payloadInfo *payload.PayloadInfo) (types.Response, error)
|
||||||
|
|
||||||
|
// SendRequest sends a prepared custom request to the specified target URL.
|
||||||
|
SendRequest(ctx context.Context, req types.Request) (types.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphQLClient is an interface that defines methods for sending
|
||||||
|
// GraphQL payloads by HTTP protocol.
|
||||||
|
type GraphQLClient interface {
|
||||||
|
// CheckAvailability checks availability of endpoint which is able to
|
||||||
|
// process GraphQL protocol messages.
|
||||||
|
CheckAvailability(ctx context.Context) (bool, error)
|
||||||
|
|
||||||
|
// IsAvailable returns status of endpoint availability.
|
||||||
|
IsAvailable() bool
|
||||||
|
|
||||||
|
// SendPayload sends a payload to the specified target URL.
|
||||||
|
SendPayload(ctx context.Context, payloadInfo *payload.PayloadInfo) (types.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GRPCClient is an interface that defines methods for sending
|
||||||
|
// payloads by gRPC protocol.
|
||||||
|
type GRPCClient interface {
|
||||||
|
// CheckAvailability checks availability of endpoint which is able to
|
||||||
|
// process gRPC protocol messages.
|
||||||
|
CheckAvailability(ctx context.Context) (bool, error)
|
||||||
|
|
||||||
|
// IsAvailable returns status of endpoint availability.
|
||||||
|
IsAvailable() bool
|
||||||
|
|
||||||
|
// SendPayload sends a payload to the specified target URL.
|
||||||
|
SendPayload(ctx context.Context, payloadInfo *payload.PayloadInfo) (types.Response, error)
|
||||||
|
|
||||||
|
// Close closes underlying connection.
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,297 @@
|
||||||
|
package gohttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/config"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/helpers"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/payload"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/payload/placeholder"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/clients"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
"github.com/wallarm/gotestwaf/pkg/dnscache"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
getCookiesRepeatAttempts = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
var redirectFunc func(req *http.Request, via []*http.Request) error
|
||||||
|
|
||||||
|
var _ clients.HTTPClient = (*Client)(nil)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
client *http.Client
|
||||||
|
headers map[string]string
|
||||||
|
hostHeader string
|
||||||
|
|
||||||
|
followCookies bool
|
||||||
|
renewSession bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(cfg *config.Config, dnsResolver *dnscache.Resolver) (*Client, error) {
|
||||||
|
tr := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: !cfg.TLSVerify},
|
||||||
|
IdleConnTimeout: time.Duration(cfg.IdleConnTimeout) * time.Second,
|
||||||
|
MaxIdleConns: cfg.MaxIdleConns,
|
||||||
|
MaxIdleConnsPerHost: cfg.MaxIdleConns, // net.http hardcodes DefaultMaxIdleConnsPerHost to 2!
|
||||||
|
}
|
||||||
|
|
||||||
|
if dnsResolver != nil {
|
||||||
|
tr.DialContext = dnscache.DialFunc(dnsResolver, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Proxy != "" {
|
||||||
|
proxyURL, err := url.Parse(cfg.Proxy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "couldn't parse proxy URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.Proxy = http.ProxyURL(proxyURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectFunc = func(req *http.Request, via []*http.Request) error {
|
||||||
|
// if maxRedirects is equal to 0 then tell the HTTP client to use
|
||||||
|
// the first HTTP response (disable following redirects)
|
||||||
|
if cfg.MaxRedirects == 0 {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(via) > cfg.MaxRedirects {
|
||||||
|
return errors.New("max redirect number exceeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: tr,
|
||||||
|
CheckRedirect: redirectFunc,
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.FollowCookies && !cfg.RenewSession {
|
||||||
|
jar, err := cookiejar.New(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Jar = jar
|
||||||
|
}
|
||||||
|
|
||||||
|
configuredHeaders := helpers.DeepCopyMap(cfg.HTTPHeaders)
|
||||||
|
customHeader := strings.SplitN(cfg.AddHeader, ":", 2)
|
||||||
|
if len(customHeader) > 1 {
|
||||||
|
header := strings.TrimSpace(customHeader[0])
|
||||||
|
value := strings.TrimSpace(customHeader[1])
|
||||||
|
configuredHeaders[header] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
client: client,
|
||||||
|
headers: configuredHeaders,
|
||||||
|
hostHeader: configuredHeaders["Host"],
|
||||||
|
followCookies: cfg.FollowCookies,
|
||||||
|
renewSession: cfg.RenewSession,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SendPayload(
|
||||||
|
ctx context.Context,
|
||||||
|
targetURL string,
|
||||||
|
payloadInfo *payload.PayloadInfo,
|
||||||
|
) (types.Response, error) {
|
||||||
|
request, err := payloadInfo.GetRequest(targetURL, types.GoHTTPClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "couldn't prepare request")
|
||||||
|
}
|
||||||
|
|
||||||
|
r, ok := request.(*types.GoHTTPRequest)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("bad request type: %T, expected %T", request, &types.GoHTTPRequest{})
|
||||||
|
}
|
||||||
|
|
||||||
|
req := r.Req.WithContext(ctx)
|
||||||
|
|
||||||
|
isUAPlaceholder := payloadInfo.PlaceholderName == placeholder.DefaultUserAgent.GetName()
|
||||||
|
|
||||||
|
for header, value := range c.headers {
|
||||||
|
// Skip setting the User-Agent header to the value from the GoTestWAF config file
|
||||||
|
// if the placeholder is UserAgent.
|
||||||
|
if strings.EqualFold(header, placeholder.UAHeader) && isUAPlaceholder {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not replace header values for RawRequest headers
|
||||||
|
if req.Header.Get(header) == "" {
|
||||||
|
req.Header.Set(header, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.Host = c.hostHeader
|
||||||
|
|
||||||
|
if payloadInfo.DebugHeaderValue != "" {
|
||||||
|
req.Header.Set(clients.GTWDebugHeader, payloadInfo.DebugHeaderValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.followCookies && c.renewSession {
|
||||||
|
cookies, err := c.getCookies(ctx, targetURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "couldn't get cookies for malicious request")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cookie := range cookies {
|
||||||
|
req.AddCookie(cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "sending http request")
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "reading response body")
|
||||||
|
}
|
||||||
|
|
||||||
|
// body reuse
|
||||||
|
resp.Body.Close()
|
||||||
|
resp.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
||||||
|
|
||||||
|
statusCode := resp.StatusCode
|
||||||
|
|
||||||
|
reasonIndex := strings.Index(resp.Status, " ")
|
||||||
|
reason := resp.Status[reasonIndex+1:]
|
||||||
|
|
||||||
|
if c.followCookies && !c.renewSession && c.client.Jar != nil {
|
||||||
|
c.client.Jar.SetCookies(req.URL, resp.Cookies())
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &types.ResponseMeta{
|
||||||
|
StatusCode: statusCode,
|
||||||
|
StatusReason: reason,
|
||||||
|
Headers: resp.Header,
|
||||||
|
Content: bodyBytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SendRequest(ctx context.Context, req types.Request) (types.Response, error) {
|
||||||
|
r, ok := req.(*types.GoHTTPRequest)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("bad request type: %T, expected %T", req, &types.GoHTTPRequest{})
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Req = r.Req.WithContext(ctx)
|
||||||
|
|
||||||
|
if c.followCookies && c.renewSession {
|
||||||
|
cookies, err := c.getCookies(ctx, helpers.GetTargetURLStr(r.Req.URL))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "couldn't get cookies for malicious request")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cookie := range cookies {
|
||||||
|
r.Req.AddCookie(cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for header, value := range c.headers {
|
||||||
|
r.Req.Header.Set(header, value)
|
||||||
|
}
|
||||||
|
r.Req.Host = c.hostHeader
|
||||||
|
|
||||||
|
if r.DebugHeaderValue != "" {
|
||||||
|
r.Req.Header.Set(clients.GTWDebugHeader, r.DebugHeaderValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.client.Do(r.Req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "sending http request")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "reading response body")
|
||||||
|
}
|
||||||
|
|
||||||
|
statusCode := resp.StatusCode
|
||||||
|
|
||||||
|
if c.followCookies && !c.renewSession && c.client.Jar != nil {
|
||||||
|
c.client.Jar.SetCookies(r.Req.URL, resp.Cookies())
|
||||||
|
}
|
||||||
|
|
||||||
|
reasonIndex := strings.Index(resp.Status, " ")
|
||||||
|
reason := resp.Status[reasonIndex+1:]
|
||||||
|
|
||||||
|
response := &types.ResponseMeta{
|
||||||
|
StatusCode: statusCode,
|
||||||
|
StatusReason: reason,
|
||||||
|
Headers: resp.Header,
|
||||||
|
Content: bodyBytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getCookies(ctx context.Context, targetURL string) ([]*http.Cookie, error) {
|
||||||
|
tr, ok := c.client.Transport.(*http.Transport)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("couldn't copy transport settings of the main HTTP to get cookies")
|
||||||
|
}
|
||||||
|
|
||||||
|
jar, err := cookiejar.New(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "couldn't create cookie jar for session renewal client")
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionClient := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: tr.DialContext,
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: tr.TLSClientConfig.InsecureSkipVerify},
|
||||||
|
IdleConnTimeout: tr.IdleConnTimeout,
|
||||||
|
MaxIdleConns: tr.MaxIdleConns,
|
||||||
|
MaxIdleConnsPerHost: tr.MaxIdleConnsPerHost,
|
||||||
|
Proxy: tr.Proxy,
|
||||||
|
},
|
||||||
|
CheckRedirect: redirectFunc,
|
||||||
|
Jar: jar,
|
||||||
|
}
|
||||||
|
|
||||||
|
var returnErr error
|
||||||
|
|
||||||
|
for i := 0; i < getCookiesRepeatAttempts; i++ {
|
||||||
|
cookiesReq, err := http.NewRequestWithContext(ctx, "GET", targetURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
returnErr = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for header, value := range c.headers {
|
||||||
|
cookiesReq.Header.Set(header, value)
|
||||||
|
}
|
||||||
|
cookiesReq.Host = c.hostHeader
|
||||||
|
|
||||||
|
cookieResp, err := sessionClient.Do(cookiesReq)
|
||||||
|
if err != nil {
|
||||||
|
returnErr = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cookieResp.Body.Close()
|
||||||
|
|
||||||
|
return sessionClient.Jar.Cookies(cookiesReq.URL), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, returnErr
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
package graphql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/wallarm/gotestwaf/internal/config"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/helpers"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/payload"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/payload/placeholder"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/clients"
|
||||||
|
"github.com/wallarm/gotestwaf/internal/scanner/types"
|
||||||
|
"github.com/wallarm/gotestwaf/pkg/dnscache"
|
||||||
|
)
|
||||||
|
|
||||||
|
// List of the possible GraphQL endpoints on URL.
|
||||||
|
var checkAvailabilityEndpoints = []string{
|
||||||
|
"/graphql",
|
||||||
|
"/_graphql",
|
||||||
|
"/api/graphql",
|
||||||
|
"/GraphQL",
|
||||||
|
}
|
||||||
|
|
||||||
|
var redirectFunc func(req *http.Request, via []*http.Request) error
|
||||||
|
|
||||||
|
var _ clients.GraphQLClient = (*Client)(nil)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
client *http.Client
|
||||||
|
headers map[string]string
|
||||||
|
hostHeader string
|
||||||
|
|
||||||
|
graphqlUrl string
|
||||||
|
httpUrl string
|
||||||
|
|
||||||
|
isGraphQLAvailable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(cfg *config.Config, dnsResolver *dnscache.Resolver) (*Client, error) {
|
||||||
|
tr := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: !cfg.TLSVerify},
|
||||||
|
IdleConnTimeout: time.Duration(cfg.IdleConnTimeout) * time.Second,
|
||||||
|
MaxIdleConns: cfg.MaxIdleConns,
|
||||||
|
MaxIdleConnsPerHost: cfg.MaxIdleConns, // net.http hardcodes DefaultMaxIdleConnsPerHost to 2!
|
||||||
|
}
|
||||||
|
|
||||||
|
if dnsResolver != nil {
|
||||||
|
tr.DialContext = dnscache.DialFunc(dnsResolver, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Proxy != "" {
|
||||||
|
proxyURL, err := url.Parse(cfg.Proxy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "couldn't parse proxy URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.Proxy = http.ProxyURL(proxyURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectFunc = func(req *http.Request, via []*http.Request) error {
|
||||||
|
// if maxRedirects is equal to 0 then tell the HTTP client to use
|
||||||
|
// the first HTTP response (disable following redirects)
|
||||||
|
if cfg.MaxRedirects == 0 {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(via) > cfg.MaxRedirects {
|
||||||
|
return errors.New("max redirect number exceeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: tr,
|
||||||
|
CheckRedirect: redirectFunc,
|
||||||
|
}
|
||||||
|
|
||||||
|
configuredHeaders := helpers.DeepCopyMap(cfg.HTTPHeaders)
|
||||||
|
customHeader := strings.SplitN(cfg.AddHeader, ":", 2)
|
||||||
|
if len(customHeader) > 1 {
|
||||||
|
header := strings.TrimSpace(customHeader[0])
|
||||||
|
value := strings.TrimSpace(customHeader[1])
|
||||||
|
configuredHeaders[header] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
client: client,
|
||||||
|
headers: configuredHeaders,
|
||||||
|
hostHeader: configuredHeaders["Host"],
|
||||||
|
|
||||||
|
graphqlUrl: cfg.GraphQLURL,
|
||||||
|
httpUrl: cfg.URL,
|
||||||
|
|
||||||
|
isGraphQLAvailable: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CheckAvailability(ctx context.Context) (bool, error) {
|
||||||
|
endpointsToCheck := checkAvailabilityEndpoints
|
||||||
|
|
||||||
|
c.isGraphQLAvailable = false
|
||||||
|
|
||||||
|
endpointURL, _ := url.Parse(c.graphqlUrl)
|
||||||
|
|
||||||
|
// Add query parameter to trigger GraphQL
|
||||||
|
queryParams := endpointURL.Query()
|
||||||
|
queryParams.Set("query", "{__typename}")
|
||||||
|
endpointURL.RawQuery = queryParams.Encode()
|
||||||
|
|
||||||
|
// If cfg.GraphQLURL is different from cfg.URL, we only need to check
|
||||||
|
// one endpoint - cfg.GraphQLURL
|
||||||
|
if c.graphqlUrl != c.httpUrl {
|
||||||
|
endpointsToCheck = []string{endpointURL.Path}
|
||||||
|
endpointURL.Path = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpoint := range endpointsToCheck {
|
||||||
|
endpointURL.Path = endpoint
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpointURL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.New("couldn't create request to check GraphQL availability")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.New("couldn't send request to check GraphQL availability")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "couldn't read response body")
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := checkAnswer(bodyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "couldn't check response")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found correct GraphQL endpoint, save it
|
||||||
|
if ok {
|
||||||
|
endpointURL.RawQuery = ""
|
||||||
|
|
||||||
|
c.graphqlUrl = endpointURL.String()
|
||||||
|
c.isGraphQLAvailable = true
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) IsAvailable() bool {
|
||||||
|
return c.isGraphQLAvailable
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SendPayload(ctx context.Context, payloadInfo *payload.PayloadInfo) (types.Response, error) {
|
||||||
|
request, err := payloadInfo.GetRequest(c.graphqlUrl, types.GoHTTPClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "couldn't prepare request")
|
||||||
|
}
|
||||||
|
|
||||||
|
r, ok := request.(*types.GoHTTPRequest)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("bad request type: %T, expected %T", request, &types.GoHTTPRequest{})
|
||||||
|
}
|
||||||
|
|
||||||
|
req := r.Req.WithContext(ctx)
|
||||||
|
|
||||||
|
isUAPlaceholder := payloadInfo.PlaceholderName == placeholder.DefaultUserAgent.GetName()
|
||||||
|
|
||||||
|
for header, value := range c.headers {
|
||||||
|
// Skip setting the User-Agent header to the value from the GoTestWAF config file
|
||||||
|
// if the placeholder is UserAgent.
|
||||||
|
if strings.EqualFold(header, placeholder.UAHeader) && isUAPlaceholder {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not replace header values for RawRequest headers
|
||||||
|
if req.Header.Get(header) == "" {
|
||||||
|
req.Header.Set(header, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.Host = c.hostHeader
|
||||||
|
|
||||||
|
if payloadInfo.DebugHeaderValue != "" {
|
||||||
|
req.Header.Set(clients.GTWDebugHeader, payloadInfo.DebugHeaderValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "sending http request")
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "reading response body")
|
||||||
|
}
|
||||||
|
|
||||||
|
// body reuse
|
||||||
|
resp.Body.Close()
|
||||||
|
resp.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
||||||
|
|
||||||
|
statusCode := resp.StatusCode
|
||||||
|
|
||||||
|
reasonIndex := strings.Index(resp.Status, " ")
|
||||||
|
reason := resp.Status[reasonIndex+1:]
|
||||||
|
|
||||||
|
response := &types.ResponseMeta{
|
||||||
|
StatusCode: statusCode,
|
||||||
|
StatusReason: reason,
|
||||||
|
Headers: resp.Header,
|
||||||
|
Content: bodyBytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAnswer checks that answer contains "__typename" in the response body.
|
||||||
|
// Example of correct answer:
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// "data": {
|
||||||
|
// "__typename": "Query"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
func checkAnswer(body []byte) (bool, error) {
|
||||||
|
jsonMap := make(map[string]any)
|
||||||
|
|
||||||
|
err := json.Unmarshal(body, &jsonMap)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "couldn't unmarshal JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, ok := jsonMap["data"]
|
||||||
|
if !ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dataMap, ok := data.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok = dataMap["__typename"]
|
||||||
|
if ok {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue