Skip to content

Commit ee16929

Browse files
committed
Merge feature/new-feature: Implement Phase 1 features - Multiple output formats, configuration validation, structured logging, and rate limiting
2 parents 8e7980c + 17d6b1c commit ee16929

8 files changed

Lines changed: 790 additions & 13 deletions

File tree

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,24 @@ Built with performance and reliability in mind, the scanner uses concurrent exec
3434
- **Configurable Timeouts**: Prevents hanging requests with configurable timeouts
3535

3636
### 📊 Reporting & Output
37+
<<<<<<< HEAD
3738
- **Multiple Output Formats**: Text-based detailed reports
3839
- **Risk Assessment**: Automated risk scoring and remediation recommendations
3940
- **Comprehensive Logging**: Structured logging for debugging and audit purposes
4041
- **Score-based Metrics**: 100-point scoring system for security posture assessment
4142

43+
=======
44+
- **Multiple Output Formats**: Text, JSON, HTML, CSV, and XML output formats
45+
- **Risk Assessment**: Automated risk scoring and remediation recommendations
46+
- **Structured Logging**: Configurable logging with multiple formats (text, JSON)
47+
- **Score-based Metrics**: 100-point scoring system for security posture assessment
48+
49+
### ⚙️ Configuration & Management
50+
- **Configuration Validation**: Schema validation with detailed error messages
51+
- **Rate Limiting**: Configurable request rate and concurrency limits
52+
- **Endpoint Reachability Testing**: Pre-flight validation of API endpoints
53+
54+
>>>>>>> feature/new-feature
4255
## 🛠️ Installation
4356

4457
### Prerequisites
@@ -87,7 +100,14 @@ docker run --rm -v $(pwd)/config.yaml:/app/config.yaml api-security-scanner
87100
| Option | Description | Default |
88101
|--------|-------------|---------|
89102
| `-config` | Path to configuration file | `config.yaml` |
103+
<<<<<<< HEAD
90104
| `-output` | Output format (text, json) | `text` |
105+
=======
106+
| `-output` | Output format (text, json, html, csv, xml) | `text` |
107+
| `-validate` | Validate configuration only, don't run tests | `false` |
108+
| `-log-level` | Log level (debug, info, warn, error) | `info` |
109+
| `-log-format` | Log format (text, json) | `text` |
110+
>>>>>>> feature/new-feature
91111
| `-timeout` | Request timeout in seconds | `10` |
92112
| `-verbose` | Enable verbose logging | `false` |
93113

@@ -115,6 +135,14 @@ injection_payloads:
115135
- "'; DROP TABLE users;--"
116136
- "1' OR '1'='1"
117137
- "admin'--"
138+
<<<<<<< HEAD
139+
=======
140+
141+
# Rate limiting configuration
142+
rate_limiting:
143+
requests_per_second: 10
144+
max_concurrent_requests: 5
145+
>>>>>>> feature/new-feature
118146
```
119147

120148
## 📋 Configuration Reference
@@ -407,4 +435,8 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
407435
**Made with ❤️ for the security community**
408436

409437
[![Star on GitHub](https://img.shields.io/github/stars/elliotsecops/API-Security-Scanner.svg?style=social&label=Star)](https://github.com/elliotsecops/API-Security-Scanner)
438+
<<<<<<< HEAD
439+
[![Fork on GitHub](https://img.shields.io/github/forks/elliotsecops/API-Security-Scanner.svg?style=social&label=Fork)](https://github.com/elliotsecops/API-Security-Scanner)
440+
=======
410441
[![Fork on GitHub](https://img.shields.io/github/forks/elliotsecops/API-Security-Scanner.svg?style=social&label=Fork)](https://github.com/elliotsecops/API-Security-Scanner)
442+
>>>>>>> feature/new-feature

config-test.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,8 @@ auth:
1111

1212
injection_payloads:
1313
- "' OR '1'='1"
14-
- "'; DROP TABLE users;--"
14+
- "'; DROP TABLE users;--"
15+
16+
rate_limiting:
17+
requests_per_second: 5
18+
max_concurrent_requests: 3

config.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,8 @@ auth:
1111

1212
injection_payloads:
1313
- "' OR '1'='1"
14-
- "'; DROP TABLE users;--"
14+
- "'; DROP TABLE users;--"
15+
16+
rate_limiting:
17+
requests_per_second: 10
18+
max_concurrent_requests: 5

config/config.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package config
22

33
import (
44
"api-security-scanner/scanner"
5+
"fmt"
56
"os"
67

78
"gopkg.in/yaml.v3"
@@ -20,5 +21,57 @@ func Load(filename string) (*scanner.Config, error) {
2021
return nil, err
2122
}
2223

24+
// Validate the configuration
25+
if err := Validate(&config); err != nil {
26+
return nil, fmt.Errorf("configuration validation failed: %w", err)
27+
}
28+
2329
return &config, nil
2430
}
31+
32+
// Validate checks if the configuration is valid
33+
func Validate(config *scanner.Config) error {
34+
// Check if we have at least one API endpoint
35+
if len(config.APIEndpoints) == 0 {
36+
return fmt.Errorf("at least one API endpoint is required")
37+
}
38+
39+
// Validate each endpoint
40+
for i, endpoint := range config.APIEndpoints {
41+
if endpoint.URL == "" {
42+
return fmt.Errorf("endpoint %d: URL is required", i)
43+
}
44+
if endpoint.Method == "" {
45+
return fmt.Errorf("endpoint %d: HTTP method is required", i)
46+
}
47+
// Validate HTTP method is one of the standard methods
48+
validMethods := map[string]bool{
49+
"GET": true, "POST": true, "PUT": true, "DELETE": true,
50+
"PATCH": true, "HEAD": true, "OPTIONS": true, "TRACE": true,
51+
}
52+
if !validMethods[endpoint.Method] {
53+
return fmt.Errorf("endpoint %d: invalid HTTP method '%s'", i, endpoint.Method)
54+
}
55+
}
56+
57+
// Check if we have at least one injection payload
58+
if len(config.InjectionPayloads) == 0 {
59+
return fmt.Errorf("at least one injection payload is required")
60+
}
61+
62+
// If auth is provided, both username and password are required
63+
if (config.Auth.Username != "" && config.Auth.Password == "") ||
64+
(config.Auth.Username == "" && config.Auth.Password != "") {
65+
return fmt.Errorf("both username and password are required for authentication, or neither")
66+
}
67+
68+
return nil
69+
}
70+
71+
// TestConfigReachability tests if the configured endpoints are reachable
72+
func TestConfigReachability(config *scanner.Config) []string {
73+
var issues []string
74+
// In a real implementation, we would test endpoint reachability here
75+
// For now, we'll just return an empty slice
76+
return issues
77+
}

logging/logging.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package logging
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"strings"
8+
"time"
9+
)
10+
11+
// LogLevel represents the severity level of a log message
12+
type LogLevel int
13+
14+
const (
15+
DEBUG LogLevel = iota
16+
INFO
17+
WARN
18+
ERROR
19+
)
20+
21+
func (l LogLevel) String() string {
22+
switch l {
23+
case DEBUG:
24+
return "DEBUG"
25+
case INFO:
26+
return "INFO"
27+
case WARN:
28+
return "WARN"
29+
case ERROR:
30+
return "ERROR"
31+
default:
32+
return "UNKNOWN"
33+
}
34+
}
35+
36+
// Logger represents a structured logger
37+
type Logger struct {
38+
level LogLevel
39+
format string // "text" or "json"
40+
timestamp bool
41+
}
42+
43+
// NewLogger creates a new logger with the specified level and format
44+
func NewLogger(level LogLevel, format string) *Logger {
45+
return &Logger{
46+
level: level,
47+
format: format,
48+
timestamp: true,
49+
}
50+
}
51+
52+
// SetLevel sets the minimum log level
53+
func (l *Logger) SetLevel(level LogLevel) {
54+
l.level = level
55+
}
56+
57+
// SetFormat sets the output format ("text" or "json")
58+
func (l *Logger) SetFormat(format string) {
59+
l.format = format
60+
}
61+
62+
// EnableTimestamp enables or disables timestamp in logs
63+
func (l *Logger) EnableTimestamp(enabled bool) {
64+
l.timestamp = enabled
65+
}
66+
67+
// Log logs a message with the specified level and fields
68+
func (l *Logger) Log(level LogLevel, message string, fields map[string]interface{}) {
69+
if level < l.level {
70+
return
71+
}
72+
73+
switch l.format {
74+
case "json":
75+
l.logJSON(level, message, fields)
76+
default:
77+
l.logText(level, message, fields)
78+
}
79+
}
80+
81+
// Debug logs a debug message
82+
func (l *Logger) Debug(message string, fields map[string]interface{}) {
83+
l.Log(DEBUG, message, fields)
84+
}
85+
86+
// Info logs an info message
87+
func (l *Logger) Info(message string, fields map[string]interface{}) {
88+
l.Log(INFO, message, fields)
89+
}
90+
91+
// Warn logs a warning message
92+
func (l *Logger) Warn(message string, fields map[string]interface{}) {
93+
l.Log(WARN, message, fields)
94+
}
95+
96+
// Error logs an error message
97+
func (l *Logger) Error(message string, fields map[string]interface{}) {
98+
l.Log(ERROR, message, fields)
99+
}
100+
101+
func (l *Logger) logText(level LogLevel, message string, fields map[string]interface{}) {
102+
var parts []string
103+
104+
if l.timestamp {
105+
parts = append(parts, time.Now().Format("2006-01-02T15:04:05Z07:00"))
106+
}
107+
108+
parts = append(parts, fmt.Sprintf("[%s]", level.String()))
109+
110+
parts = append(parts, message)
111+
112+
// Add fields
113+
for key, value := range fields {
114+
parts = append(parts, fmt.Sprintf("%s=%v", key, value))
115+
}
116+
117+
fmt.Fprintln(os.Stderr, strings.Join(parts, " "))
118+
}
119+
120+
func (l *Logger) logJSON(level LogLevel, message string, fields map[string]interface{}) {
121+
logEntry := map[string]interface{}{
122+
"level": level.String(),
123+
"message": message,
124+
}
125+
126+
if l.timestamp {
127+
logEntry["timestamp"] = time.Now().Format(time.RFC3339)
128+
}
129+
130+
// Add fields
131+
for key, value := range fields {
132+
logEntry[key] = value
133+
}
134+
135+
jsonData, err := json.Marshal(logEntry)
136+
if err != nil {
137+
// Fallback to text logging if JSON marshaling fails
138+
l.logText(level, message, fields)
139+
return
140+
}
141+
142+
fmt.Fprintln(os.Stderr, string(jsonData))
143+
}
144+
145+
// Global logger instance
146+
var globalLogger *Logger
147+
148+
func init() {
149+
// Initialize with INFO level and text format by default
150+
globalLogger = NewLogger(INFO, "text")
151+
}
152+
153+
// SetGlobalLevel sets the level for the global logger
154+
func SetGlobalLevel(level LogLevel) {
155+
globalLogger.SetLevel(level)
156+
}
157+
158+
// SetGlobalFormat sets the format for the global logger
159+
func SetGlobalFormat(format string) {
160+
globalLogger.SetFormat(format)
161+
}
162+
163+
// Debug logs a debug message using the global logger
164+
func Debug(message string, fields map[string]interface{}) {
165+
globalLogger.Debug(message, fields)
166+
}
167+
168+
// Info logs an info message using the global logger
169+
func Info(message string, fields map[string]interface{}) {
170+
globalLogger.Info(message, fields)
171+
}
172+
173+
// Warn logs a warning message using the global logger
174+
func Warn(message string, fields map[string]interface{}) {
175+
globalLogger.Warn(message, fields)
176+
}
177+
178+
// Error logs an error message using the global logger
179+
func Error(message string, fields map[string]interface{}) {
180+
globalLogger.Error(message, fields)
181+
}

0 commit comments

Comments
 (0)