hedra 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +342 -0
- data/bin/hedra +14 -0
- data/config/example_config.yml +17 -0
- data/config/example_rules.yml +16 -0
- data/lib/hedra/analyzer.rb +216 -0
- data/lib/hedra/cli.rb +336 -0
- data/lib/hedra/config.rb +46 -0
- data/lib/hedra/exporter.rb +49 -0
- data/lib/hedra/http_client.rb +69 -0
- data/lib/hedra/plugin_manager.rb +70 -0
- data/lib/hedra/scorer.rb +54 -0
- data/lib/hedra/version.rb +5 -0
- data/lib/hedra.rb +16 -0
- metadata +141 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ab829bc24159c4c23c53be1f82efbc8f61374401c2ae7f882140c8bb8de8e37f
|
|
4
|
+
data.tar.gz: 9a83e90dfb32a981e978c57c37973dee68d55adfdebe071517127ce9a98f1d92
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2f499c341d3ed325fe3be5eb4e9ab6db27a0e3c54441c9dc19d48a6631d79f8c0dcfea050f6d4cc7322c7bcba4d9d3a7149a71044db42bae3c697edc4e360956
|
|
7
|
+
data.tar.gz: 8b242f020e24deb65b6c218938532d43ddcdc15963015e644c8cd7edc4c5244d04547e38133fae18f139841dd37244a46387186660a2fec4b236257a0f9e5fea
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Hedra Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# Hedra 🛡️
|
|
2
|
+
|
|
3
|
+
A comprehensive security header analyzer for modern web applications. Scan, audit, and monitor HTTP security headers with ease.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
_ _ _
|
|
7
|
+
| | | | ___ __| |_ __ __ _
|
|
8
|
+
| |_| |/ _ \/ _` | '__/ _` |
|
|
9
|
+
| _ | __/ (_| | | | (_| |
|
|
10
|
+
|_| |_|\___|\__,_|_| \__,_|
|
|
11
|
+
|
|
12
|
+
Security Header Analyzer
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- 🔍 **Comprehensive Scanning** - Analyze security headers for single or multiple URLs
|
|
18
|
+
- 🎯 **Deep Auditing** - Detailed security header analysis with recommendations
|
|
19
|
+
- 👁️ **Continuous Monitoring** - Watch URLs for header changes over time
|
|
20
|
+
- 📊 **Multiple Output Formats** - Table, JSON, and CSV export options
|
|
21
|
+
- 🔌 **Plugin Architecture** - Extend with custom header checks
|
|
22
|
+
- ⚡ **Concurrent Scanning** - Fast parallel URL scanning with configurable concurrency
|
|
23
|
+
- 🌐 **Proxy Support** - HTTP and SOCKS proxy compatibility
|
|
24
|
+
- 🎨 **Beautiful CLI** - Color-coded output with severity badges
|
|
25
|
+
- 📈 **Security Scoring** - 0-100 score based on header coverage
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
### From Source
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Clone the repository
|
|
33
|
+
git clone https://github.com/hedra/hedra.git
|
|
34
|
+
cd hedra
|
|
35
|
+
|
|
36
|
+
# Install dependencies
|
|
37
|
+
bundle install
|
|
38
|
+
|
|
39
|
+
# Build the gem
|
|
40
|
+
rake build
|
|
41
|
+
|
|
42
|
+
# Install the gem
|
|
43
|
+
gem install pkg/hedra-1.0.0.gem
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Quick Start
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
bundle install
|
|
50
|
+
chmod +x bin/hedra
|
|
51
|
+
bin/hedra --help
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
### Basic Scanning
|
|
57
|
+
|
|
58
|
+
Scan a single URL:
|
|
59
|
+
```bash
|
|
60
|
+
hedra scan https://example.com
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Scan multiple URLs from a file:
|
|
64
|
+
```bash
|
|
65
|
+
hedra scan -f urls.txt
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Deep Audit
|
|
69
|
+
|
|
70
|
+
Perform detailed security analysis:
|
|
71
|
+
```bash
|
|
72
|
+
hedra audit https://example.com
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Export audit results as JSON:
|
|
76
|
+
```bash
|
|
77
|
+
hedra audit https://example.com --json --output result.json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Advanced Scanning
|
|
81
|
+
|
|
82
|
+
Concurrent scanning with custom settings:
|
|
83
|
+
```bash
|
|
84
|
+
hedra scan -f urls.txt --concurrency 20 --timeout 15
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Scan through a proxy:
|
|
88
|
+
```bash
|
|
89
|
+
hedra scan https://example.com --proxy http://127.0.0.1:8080
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Custom User-Agent and follow redirects:
|
|
93
|
+
```bash
|
|
94
|
+
hedra scan https://example.com --user-agent "MyBot/1.0" --follow-redirects
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Continuous Monitoring
|
|
98
|
+
|
|
99
|
+
Watch a URL and check every hour:
|
|
100
|
+
```bash
|
|
101
|
+
hedra watch https://example.com --interval 3600
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Compare Headers
|
|
105
|
+
|
|
106
|
+
Compare security headers between two URLs:
|
|
107
|
+
```bash
|
|
108
|
+
hedra compare https://staging.example.com https://prod.example.com
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Export Results
|
|
112
|
+
|
|
113
|
+
Export scan results:
|
|
114
|
+
```bash
|
|
115
|
+
hedra scan -f urls.txt --output results.csv --format csv
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Plugin Management
|
|
119
|
+
|
|
120
|
+
List installed plugins:
|
|
121
|
+
```bash
|
|
122
|
+
hedra plugin list
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Install a custom plugin:
|
|
126
|
+
```bash
|
|
127
|
+
hedra plugin install path/to/plugin.rb
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Remove a plugin:
|
|
131
|
+
```bash
|
|
132
|
+
hedra plugin remove my_plugin
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Security Headers Checked
|
|
136
|
+
|
|
137
|
+
Hedra analyzes the following security headers:
|
|
138
|
+
|
|
139
|
+
### Critical Headers
|
|
140
|
+
- **Content-Security-Policy (CSP)** - Prevents XSS and injection attacks
|
|
141
|
+
- **Strict-Transport-Security (HSTS)** - Enforces HTTPS connections
|
|
142
|
+
|
|
143
|
+
### Important Headers
|
|
144
|
+
- **X-Frame-Options** - Prevents clickjacking attacks
|
|
145
|
+
- **X-Content-Type-Options** - Prevents MIME-sniffing attacks
|
|
146
|
+
|
|
147
|
+
### Recommended Headers
|
|
148
|
+
- **Referrer-Policy** - Controls referrer information
|
|
149
|
+
- **Permissions-Policy** - Controls browser features
|
|
150
|
+
- **Cross-Origin-Opener-Policy (COOP)** - Isolates browsing context
|
|
151
|
+
- **Cross-Origin-Embedder-Policy (COEP)** - Controls resource embedding
|
|
152
|
+
- **Cross-Origin-Resource-Policy (CORP)** - Controls resource sharing
|
|
153
|
+
|
|
154
|
+
## Configuration
|
|
155
|
+
|
|
156
|
+
Create a config file at `~/.hedra/config.yml`:
|
|
157
|
+
|
|
158
|
+
```yaml
|
|
159
|
+
timeout: 10
|
|
160
|
+
concurrency: 10
|
|
161
|
+
follow_redirects: false
|
|
162
|
+
user_agent: "Hedra/1.0.0"
|
|
163
|
+
output_format: table
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Custom Rules
|
|
167
|
+
|
|
168
|
+
Add custom header checks in `~/.hedra/rules.yml`:
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
rules:
|
|
172
|
+
- header: "X-Custom-Security"
|
|
173
|
+
type: missing
|
|
174
|
+
severity: warning
|
|
175
|
+
message: "Custom security header is missing"
|
|
176
|
+
fix: "Add X-Custom-Security header"
|
|
177
|
+
|
|
178
|
+
- header: "Server"
|
|
179
|
+
type: pattern
|
|
180
|
+
pattern: "(Apache|nginx|IIS)"
|
|
181
|
+
severity: info
|
|
182
|
+
message: "Server header exposes server software"
|
|
183
|
+
fix: "Remove or obfuscate Server header"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Plugin Development
|
|
187
|
+
|
|
188
|
+
Create custom plugins to extend Hedra's functionality:
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
# ~/.hedra/plugins/my_plugin.rb
|
|
192
|
+
module Hedra
|
|
193
|
+
class MyPlugin < Plugin
|
|
194
|
+
def self.check(headers)
|
|
195
|
+
findings = []
|
|
196
|
+
|
|
197
|
+
unless headers.key?('x-my-header')
|
|
198
|
+
findings << {
|
|
199
|
+
header: 'x-my-header',
|
|
200
|
+
issue: 'My custom header is missing',
|
|
201
|
+
severity: :warning,
|
|
202
|
+
recommended_fix: 'Add X-My-Header'
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
findings
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Output Examples
|
|
213
|
+
|
|
214
|
+
### Table Output
|
|
215
|
+
```
|
|
216
|
+
https://example.com
|
|
217
|
+
Score: 75/100
|
|
218
|
+
Timestamp: 2025-11-12T10:30:00Z
|
|
219
|
+
|
|
220
|
+
┌─────────────────────────────┬──────────────────────────────┬──────────┐
|
|
221
|
+
│ Header │ Issue │ Severity │
|
|
222
|
+
├─────────────────────────────┼──────────────────────────────┼──────────┤
|
|
223
|
+
│ permissions-policy │ Header is missing │ ● INFO │
|
|
224
|
+
│ cross-origin-opener-policy │ Header is missing │ ● INFO │
|
|
225
|
+
└─────────────────────────────┴──────────────────────────────┴──────────┘
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### JSON Output
|
|
229
|
+
```json
|
|
230
|
+
{
|
|
231
|
+
"url": "https://example.com",
|
|
232
|
+
"timestamp": "2025-11-12T10:30:00Z",
|
|
233
|
+
"headers": {
|
|
234
|
+
"content-security-policy": "default-src 'self'",
|
|
235
|
+
"strict-transport-security": "max-age=31536000"
|
|
236
|
+
},
|
|
237
|
+
"findings": [
|
|
238
|
+
{
|
|
239
|
+
"header": "x-frame-options",
|
|
240
|
+
"issue": "X-Frame-Options header is missing",
|
|
241
|
+
"severity": "warning",
|
|
242
|
+
"recommended_fix": "Add X-Frame-Options: DENY or SAMEORIGIN"
|
|
243
|
+
}
|
|
244
|
+
],
|
|
245
|
+
"score": 75
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Development
|
|
250
|
+
|
|
251
|
+
### Running Tests
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
# Run all tests
|
|
255
|
+
bundle exec rspec
|
|
256
|
+
|
|
257
|
+
# Run with coverage
|
|
258
|
+
bundle exec rspec --format documentation
|
|
259
|
+
|
|
260
|
+
# Run specific test file
|
|
261
|
+
bundle exec rspec spec/hedra/analyzer_spec.rb
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Linting
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
# Run RuboCop
|
|
268
|
+
bundle exec rubocop
|
|
269
|
+
|
|
270
|
+
# Auto-fix issues
|
|
271
|
+
bundle exec rubocop -a
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Building
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
# Build gem
|
|
278
|
+
rake build
|
|
279
|
+
|
|
280
|
+
# Install locally
|
|
281
|
+
gem install pkg/hedra-1.0.0.gem
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## CI/CD
|
|
285
|
+
|
|
286
|
+
Hedra includes GitHub Actions CI configuration that:
|
|
287
|
+
- Runs tests on Ruby 3.0, 3.1, and 3.2
|
|
288
|
+
- Executes RuboCop linting
|
|
289
|
+
- Builds the gem package
|
|
290
|
+
|
|
291
|
+
## Architecture
|
|
292
|
+
|
|
293
|
+
### Core Components
|
|
294
|
+
|
|
295
|
+
- **CLI** - Thor-based command-line interface with subcommands
|
|
296
|
+
- **Analyzer** - Core logic for header analysis and validation
|
|
297
|
+
- **HttpClient** - HTTP wrapper with retry logic, proxy support, and TLS verification
|
|
298
|
+
- **Scorer** - Calculates security scores based on header coverage
|
|
299
|
+
- **PluginManager** - Discovers and executes custom plugins
|
|
300
|
+
- **Exporter** - Handles JSON and CSV output formats
|
|
301
|
+
|
|
302
|
+
### Design Decisions
|
|
303
|
+
|
|
304
|
+
1. **Modular Architecture** - Each header check is isolated, making it easy to add new checks
|
|
305
|
+
2. **Secure Defaults** - TLS verification on, no redirect following, conservative timeouts
|
|
306
|
+
3. **Thread-Safe Concurrency** - Uses Ruby's concurrent-ruby gem for safe parallel scanning
|
|
307
|
+
4. **Extensible Plugin System** - Simple base class for custom header checks
|
|
308
|
+
5. **Comprehensive Testing** - WebMock stubs prevent live network calls in tests
|
|
309
|
+
|
|
310
|
+
## Contributing
|
|
311
|
+
|
|
312
|
+
1. Fork the repository
|
|
313
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
314
|
+
3. Write tests for your changes
|
|
315
|
+
4. Ensure tests pass (`bundle exec rspec`)
|
|
316
|
+
5. Ensure linting passes (`bundle exec rubocop`)
|
|
317
|
+
6. Commit your changes (`git commit -am 'Add amazing feature'`)
|
|
318
|
+
7. Push to the branch (`git push origin feature/amazing-feature`)
|
|
319
|
+
8. Open a Pull Request
|
|
320
|
+
|
|
321
|
+
## License
|
|
322
|
+
|
|
323
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
324
|
+
|
|
325
|
+
## Support
|
|
326
|
+
|
|
327
|
+
- 📖 Documentation: [GitHub Wiki](https://github.com/hedra/hedra/wiki)
|
|
328
|
+
- 🐛 Issues: [GitHub Issues](https://github.com/hedra/hedra/issues)
|
|
329
|
+
- 💬 Discussions: [GitHub Discussions](https://github.com/hedra/hedra/discussions)
|
|
330
|
+
|
|
331
|
+
## Acknowledgments
|
|
332
|
+
|
|
333
|
+
Built with:
|
|
334
|
+
- [Thor](https://github.com/rails/thor) - CLI framework
|
|
335
|
+
- [HTTP.rb](https://github.com/httprb/http) - HTTP client
|
|
336
|
+
- [TTY::Table](https://github.com/piotrmurach/tty-table) - Terminal tables
|
|
337
|
+
- [Pastel](https://github.com/piotrmurach/pastel) - Terminal colors
|
|
338
|
+
- [RSpec](https://rspec.info/) - Testing framework
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
Made with ❤️ by the Hedra Team
|
data/bin/hedra
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Hedra Configuration Example
|
|
2
|
+
# Copy to ~/.hedra/config.yml to customize
|
|
3
|
+
|
|
4
|
+
# HTTP client settings
|
|
5
|
+
timeout: 10
|
|
6
|
+
concurrency: 10
|
|
7
|
+
follow_redirects: false
|
|
8
|
+
user_agent: "Hedra/1.0.0"
|
|
9
|
+
|
|
10
|
+
# Proxy settings (optional)
|
|
11
|
+
# proxy: "http://127.0.0.1:8080"
|
|
12
|
+
|
|
13
|
+
# Output preferences
|
|
14
|
+
output_format: table # table, json, or csv
|
|
15
|
+
|
|
16
|
+
# Rate limiting (optional)
|
|
17
|
+
# rate_limit: "5/s"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Custom security header rules
|
|
2
|
+
# Copy to ~/.hedra/rules.yml to enable
|
|
3
|
+
|
|
4
|
+
rules:
|
|
5
|
+
- header: "X-Custom-Security"
|
|
6
|
+
type: missing
|
|
7
|
+
severity: warning
|
|
8
|
+
message: "Custom security header is missing"
|
|
9
|
+
fix: "Add X-Custom-Security header"
|
|
10
|
+
|
|
11
|
+
- header: "Server"
|
|
12
|
+
type: pattern
|
|
13
|
+
pattern: "(Apache|nginx|IIS)"
|
|
14
|
+
severity: info
|
|
15
|
+
message: "Server header exposes server software"
|
|
16
|
+
fix: "Remove or obfuscate Server header"
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module Hedra
|
|
6
|
+
class Analyzer
|
|
7
|
+
SECURITY_HEADERS = {
|
|
8
|
+
'content-security-policy' => {
|
|
9
|
+
required: true,
|
|
10
|
+
severity: :critical,
|
|
11
|
+
message: 'Content-Security-Policy header is missing',
|
|
12
|
+
fix: "Add CSP header: Content-Security-Policy: default-src 'self'"
|
|
13
|
+
},
|
|
14
|
+
'strict-transport-security' => {
|
|
15
|
+
required: true,
|
|
16
|
+
severity: :critical,
|
|
17
|
+
message: 'Strict-Transport-Security (HSTS) header is missing',
|
|
18
|
+
fix: 'Add HSTS header: Strict-Transport-Security: max-age=31536000; includeSubDomains'
|
|
19
|
+
},
|
|
20
|
+
'x-frame-options' => {
|
|
21
|
+
required: true,
|
|
22
|
+
severity: :warning,
|
|
23
|
+
message: 'X-Frame-Options header is missing',
|
|
24
|
+
fix: 'Add X-Frame-Options: DENY or SAMEORIGIN'
|
|
25
|
+
},
|
|
26
|
+
'x-content-type-options' => {
|
|
27
|
+
required: true,
|
|
28
|
+
severity: :warning,
|
|
29
|
+
message: 'X-Content-Type-Options header is missing',
|
|
30
|
+
fix: 'Add X-Content-Type-Options: nosniff'
|
|
31
|
+
},
|
|
32
|
+
'referrer-policy' => {
|
|
33
|
+
required: true,
|
|
34
|
+
severity: :info,
|
|
35
|
+
message: 'Referrer-Policy header is missing',
|
|
36
|
+
fix: 'Add Referrer-Policy: strict-origin-when-cross-origin'
|
|
37
|
+
},
|
|
38
|
+
'permissions-policy' => {
|
|
39
|
+
required: false,
|
|
40
|
+
severity: :info,
|
|
41
|
+
message: 'Permissions-Policy header is missing',
|
|
42
|
+
fix: 'Consider adding Permissions-Policy to control browser features'
|
|
43
|
+
},
|
|
44
|
+
'cross-origin-opener-policy' => {
|
|
45
|
+
required: false,
|
|
46
|
+
severity: :info,
|
|
47
|
+
message: 'Cross-Origin-Opener-Policy header is missing',
|
|
48
|
+
fix: 'Add Cross-Origin-Opener-Policy: same-origin'
|
|
49
|
+
},
|
|
50
|
+
'cross-origin-embedder-policy' => {
|
|
51
|
+
required: false,
|
|
52
|
+
severity: :info,
|
|
53
|
+
message: 'Cross-Origin-Embedder-Policy header is missing',
|
|
54
|
+
fix: 'Add Cross-Origin-Embedder-Policy: require-corp'
|
|
55
|
+
},
|
|
56
|
+
'cross-origin-resource-policy' => {
|
|
57
|
+
required: false,
|
|
58
|
+
severity: :info,
|
|
59
|
+
message: 'Cross-Origin-Resource-Policy header is missing',
|
|
60
|
+
fix: 'Add Cross-Origin-Resource-Policy: same-origin'
|
|
61
|
+
}
|
|
62
|
+
}.freeze
|
|
63
|
+
|
|
64
|
+
def initialize
|
|
65
|
+
@plugin_manager = PluginManager.new
|
|
66
|
+
@scorer = Scorer.new
|
|
67
|
+
load_custom_rules
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def analyze(url, headers)
|
|
71
|
+
normalized_headers = normalize_headers(headers)
|
|
72
|
+
findings = []
|
|
73
|
+
|
|
74
|
+
# Check for missing required headers
|
|
75
|
+
SECURITY_HEADERS.each do |header_name, config|
|
|
76
|
+
next unless config[:required]
|
|
77
|
+
|
|
78
|
+
next if normalized_headers.key?(header_name)
|
|
79
|
+
|
|
80
|
+
findings << {
|
|
81
|
+
header: header_name,
|
|
82
|
+
issue: config[:message],
|
|
83
|
+
severity: config[:severity],
|
|
84
|
+
recommended_fix: config[:fix]
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Validate header values
|
|
89
|
+
findings.concat(validate_header_values(normalized_headers))
|
|
90
|
+
|
|
91
|
+
# Apply custom rules
|
|
92
|
+
findings.concat(apply_custom_rules(normalized_headers))
|
|
93
|
+
|
|
94
|
+
# Run plugin checks
|
|
95
|
+
findings.concat(@plugin_manager.run_checks(normalized_headers))
|
|
96
|
+
|
|
97
|
+
# Calculate security score
|
|
98
|
+
score = @scorer.calculate(normalized_headers, findings)
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
url: url,
|
|
102
|
+
timestamp: Time.now.iso8601,
|
|
103
|
+
headers: headers,
|
|
104
|
+
findings: findings,
|
|
105
|
+
score: score
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def normalize_headers(headers)
|
|
112
|
+
headers.transform_keys { |k| k.to_s.downcase }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def validate_header_values(headers)
|
|
116
|
+
findings = []
|
|
117
|
+
|
|
118
|
+
# Validate CSP
|
|
119
|
+
if headers['content-security-policy']
|
|
120
|
+
csp = headers['content-security-policy']
|
|
121
|
+
if csp.include?('unsafe-inline') || csp.include?('unsafe-eval')
|
|
122
|
+
findings << {
|
|
123
|
+
header: 'content-security-policy',
|
|
124
|
+
issue: 'CSP contains unsafe directives (unsafe-inline or unsafe-eval)',
|
|
125
|
+
severity: :warning,
|
|
126
|
+
recommended_fix: 'Remove unsafe-inline and unsafe-eval, use nonces or hashes'
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Validate HSTS
|
|
132
|
+
if headers['strict-transport-security']
|
|
133
|
+
hsts = headers['strict-transport-security']
|
|
134
|
+
if hsts =~ /max-age=(\d+)/
|
|
135
|
+
max_age = ::Regexp.last_match(1).to_i
|
|
136
|
+
if max_age < 31_536_000
|
|
137
|
+
findings << {
|
|
138
|
+
header: 'strict-transport-security',
|
|
139
|
+
issue: 'HSTS max-age is less than 1 year (31536000 seconds)',
|
|
140
|
+
severity: :warning,
|
|
141
|
+
recommended_fix: 'Set max-age to at least 31536000'
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Validate X-Frame-Options
|
|
148
|
+
if headers['x-frame-options']
|
|
149
|
+
xfo = headers['x-frame-options'].upcase
|
|
150
|
+
unless %w[DENY SAMEORIGIN].include?(xfo.split.first)
|
|
151
|
+
findings << {
|
|
152
|
+
header: 'x-frame-options',
|
|
153
|
+
issue: 'X-Frame-Options has invalid value',
|
|
154
|
+
severity: :warning,
|
|
155
|
+
recommended_fix: 'Use DENY or SAMEORIGIN'
|
|
156
|
+
}
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Validate X-Content-Type-Options
|
|
161
|
+
if headers['x-content-type-options']
|
|
162
|
+
xcto = headers['x-content-type-options'].downcase
|
|
163
|
+
unless xcto == 'nosniff'
|
|
164
|
+
findings << {
|
|
165
|
+
header: 'x-content-type-options',
|
|
166
|
+
issue: 'X-Content-Type-Options should be "nosniff"',
|
|
167
|
+
severity: :info,
|
|
168
|
+
recommended_fix: 'Set to nosniff'
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
findings
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def load_custom_rules
|
|
177
|
+
@custom_rules = []
|
|
178
|
+
config_path = File.expand_path('~/.hedra/rules.yml')
|
|
179
|
+
return unless File.exist?(config_path)
|
|
180
|
+
|
|
181
|
+
rules = YAML.load_file(config_path)
|
|
182
|
+
@custom_rules = rules['rules'] || []
|
|
183
|
+
rescue StandardError => e
|
|
184
|
+
warn "Failed to load custom rules: #{e.message}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def apply_custom_rules(headers)
|
|
188
|
+
findings = []
|
|
189
|
+
|
|
190
|
+
@custom_rules.each do |rule|
|
|
191
|
+
header_name = rule['header'].downcase
|
|
192
|
+
pattern = Regexp.new(rule['pattern']) if rule['pattern']
|
|
193
|
+
|
|
194
|
+
if rule['type'] == 'missing' && !headers.key?(header_name)
|
|
195
|
+
findings << {
|
|
196
|
+
header: header_name,
|
|
197
|
+
issue: rule['message'],
|
|
198
|
+
severity: rule['severity'].to_sym,
|
|
199
|
+
recommended_fix: rule['fix']
|
|
200
|
+
}
|
|
201
|
+
elsif rule['type'] == 'pattern' && headers[header_name]
|
|
202
|
+
if pattern && headers[header_name] =~ pattern
|
|
203
|
+
findings << {
|
|
204
|
+
header: header_name,
|
|
205
|
+
issue: rule['message'],
|
|
206
|
+
severity: rule['severity'].to_sym,
|
|
207
|
+
recommended_fix: rule['fix']
|
|
208
|
+
}
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
findings
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|