Skip to content

Commit 83f001d

Browse files
Add PII ruleset and ruleset includes support
1 parent 41e11ec commit 83f001d

5 files changed

Lines changed: 130 additions & 4 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "log-security-analyzer"
3-
version = "1.0.0"
3+
version = "1.1.0"
44
edition = "2021"
55

66
[dependencies]

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# Log Security Analyzer
44

5-
![Version: 1.0.0](https://img.shields.io/badge/version-1.0.0-blue)
5+
![Version: 1.1.0](https://img.shields.io/badge/version-1.1.0-blue)
66
![License: MIT](https://img.shields.io/badge/License-MIT-blue)
77

88
A Rust CLI tool to scan log files and detect exposed secrets (tokens, API keys,
@@ -12,6 +12,7 @@ credentials) using configurable regex rules in TOML format.
1212

1313
- Line-by-line scanning of arbitrary log files
1414
- Detection rules defined in TOML files, easily extensible
15+
- Ruleset includes via `includes` field for composable rulesets
1516
- Advanced regex support (lookahead/lookbehind) via `fancy-regex`
1617
- Severity levels: `critical`, `high`, `medium`, `low`
1718
- Formatted table output with color-coded severity
@@ -32,6 +33,13 @@ The default ruleset (`rulesets/default.toml`) detects:
3233
| MySQL Connection String | high |
3334
| JWT Token | high |
3435

36+
The PII ruleset (`rulesets/pii.toml`) includes the default rules and adds:
37+
38+
| Secret | Severity |
39+
|---------------------|----------|
40+
| Italian Fiscal Code | high |
41+
| IBAN | high |
42+
3543
## Requirements
3644

3745
- Rust 1.70+
@@ -79,6 +87,21 @@ severity = "high"
7987

8088
Valid values for `severity`: `critical`, `high`, `medium`, `low`.
8189

90+
You can include rules from other rulesets using the `includes` field:
91+
92+
```toml
93+
includes = ["default.toml"]
94+
95+
[[rules]]
96+
id = "my-rule"
97+
description = "Additional rule"
98+
regex = '''pattern_regex'''
99+
tags = ["tag1"]
100+
severity = "high"
101+
```
102+
103+
Paths are resolved relative to the including file.
104+
82105
## Project Structure
83106

84107
```
@@ -90,6 +113,7 @@ src/
90113
severity.rs # Severity level enum
91114
rulesets/
92115
default.toml # Default ruleset
116+
pii.toml # PII ruleset (includes default)
93117
logs/
94118
app.log # Sample log file
95119
```

rulesets/pii.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
title = "PII Detection Rules"
2+
description = "Rules for detecting personally identifiable information in logs"
3+
includes = ["default.toml"]
4+
5+
[[rules]]
6+
id = "italian-fiscal-code"
7+
description = "Italian Fiscal Code"
8+
regex = '''(?i)\b[A-Z]{6}[0-9]{2}[A-EHLMPRST][0-9]{2}[A-Z][0-9]{3}[A-Z]\b'''
9+
tags = ["pii", "fiscal-code", "italy", "log", "runtime"]
10+
severity = "high"
11+
12+
[[rules]]
13+
id = "iban"
14+
description = "IBAN"
15+
regex = '''(?i)\b[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}\b'''
16+
tags = ["pii", "iban", "finance", "log", "runtime"]
17+
severity = "high"

src/rules.rs

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
use crate::severity::Severity;
44
use fancy_regex::Regex;
55
use log::warn;
6+
use std::collections::HashSet;
67
use std::io;
7-
use std::path::Path;
8+
use std::path::{Path, PathBuf};
89

910
/// Represents a secret detection rule.
1011
pub struct Rule {
@@ -15,12 +16,36 @@ pub struct Rule {
1516

1617
/// Loads rules from a TOML file.
1718
pub fn load_rules<P: AsRef<Path>>(path: P) -> io::Result<Vec<Rule>> {
19+
let mut visited = HashSet::new();
20+
load_rules_inner(path.as_ref(), &mut visited)
21+
}
22+
23+
/// Recursively loads rules, tracking visited files to detect circular includes.
24+
fn load_rules_inner(path: &Path, visited: &mut HashSet<PathBuf>) -> io::Result<Vec<Rule>> {
25+
let canonical = path.canonicalize()?;
26+
if !visited.insert(canonical) {
27+
return Err(io::Error::new(
28+
io::ErrorKind::InvalidData,
29+
format!("Circular include detected: {}", path.display()),
30+
));
31+
}
32+
33+
let base_dir = path.parent().unwrap_or(Path::new("."));
1834
let content = std::fs::read_to_string(path)?;
1935
let toml_value: toml::Value =
2036
toml::from_str(&content).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
2137

2238
let mut rules = Vec::new();
2339

40+
if let Some(includes) = toml_value.get("includes").and_then(|v| v.as_array()) {
41+
for include in includes {
42+
if let Some(include_path) = include.as_str() {
43+
let full_path = base_dir.join(include_path);
44+
rules.extend(load_rules_inner(&full_path, visited)?);
45+
}
46+
}
47+
}
48+
2449
if let Some(rules_array) = toml_value.get("rules").and_then(|v| v.as_array()) {
2550
for rule in rules_array {
2651
let description = match rule.get("description").and_then(|v| v.as_str()) {
@@ -80,4 +105,64 @@ mod tests {
80105
assert_eq!(rules[0].severity, Severity::High);
81106
assert!(rules[0].regex.is_match("tok_123").unwrap());
82107
}
108+
109+
#[test]
110+
fn test_load_rules_with_includes() {
111+
let base_toml = r#"
112+
[[rules]]
113+
description = "Base Rule"
114+
regex = "base_[0-9]+"
115+
severity = "low"
116+
"#;
117+
118+
let child_toml = r#"
119+
includes = ["base_rules.toml"]
120+
121+
[[rules]]
122+
description = "Child Rule"
123+
regex = "child_[0-9]+"
124+
severity = "high"
125+
"#;
126+
127+
let tmp_dir = std::env::temp_dir().join("lsa_test_includes");
128+
std::fs::create_dir_all(&tmp_dir).unwrap();
129+
130+
std::fs::write(tmp_dir.join("base_rules.toml"), base_toml).unwrap();
131+
std::fs::write(tmp_dir.join("child_rules.toml"), child_toml).unwrap();
132+
133+
let rules = load_rules(tmp_dir.join("child_rules.toml")).unwrap();
134+
assert_eq!(rules.len(), 2);
135+
assert_eq!(rules[0].description, "Base Rule");
136+
assert_eq!(rules[1].description, "Child Rule");
137+
}
138+
139+
#[test]
140+
fn test_load_rules_circular_include() {
141+
let a_toml = r#"
142+
includes = ["b.toml"]
143+
144+
[[rules]]
145+
description = "Rule A"
146+
regex = "a_[0-9]+"
147+
severity = "low"
148+
"#;
149+
150+
let b_toml = r#"
151+
includes = ["a.toml"]
152+
153+
[[rules]]
154+
description = "Rule B"
155+
regex = "b_[0-9]+"
156+
severity = "low"
157+
"#;
158+
159+
let tmp_dir = std::env::temp_dir().join("lsa_test_circular");
160+
std::fs::create_dir_all(&tmp_dir).unwrap();
161+
162+
std::fs::write(tmp_dir.join("a.toml"), a_toml).unwrap();
163+
std::fs::write(tmp_dir.join("b.toml"), b_toml).unwrap();
164+
165+
let result = load_rules(tmp_dir.join("a.toml"));
166+
assert!(result.is_err());
167+
}
83168
}

0 commit comments

Comments
 (0)