git-ward 0.1.0 → 0.1.3
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.
- package/Cargo.toml +1 -1
- package/LICENSE +1 -1
- package/README.md +13 -2
- package/package.json +1 -1
- package/src/config.rs +18 -6
- package/src/git.rs +23 -0
- package/src/main.rs +13 -3
- package/src/scanner.rs +21 -9
package/Cargo.toml
CHANGED
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
## 🚀 Features
|
|
6
6
|
|
|
7
|
-
- **Zero Friction:** Scans
|
|
7
|
+
- **Zero Friction:** Scans _staged_ files in < 50ms (ignoring working directory changes).
|
|
8
8
|
- **Privacy First:** All scanning happens locally. No data leaves your machine.
|
|
9
9
|
- **Smart Detection:**
|
|
10
10
|
- **Ethereum Private Keys**
|
|
11
11
|
- **BIP-39 Mnemonics**
|
|
12
12
|
- **Generic API Keys**
|
|
13
|
+
- **.env Files** (Blocks `.env`, `.env.local`, etc. Allows `.example`/`.sample`)
|
|
13
14
|
- **High Entropy Strings** (with false positive filtering)
|
|
14
|
-
- **Configurable:** Ignore specific files
|
|
15
|
+
- **Configurable:** Ignore specific files via `.wardignore` or `ward.toml`.
|
|
15
16
|
|
|
16
17
|
## 📦 Installation
|
|
17
18
|
|
|
@@ -76,6 +77,16 @@ name = "My Custom Token"
|
|
|
76
77
|
regex = "MYTOKEN-[0-9]{5}"
|
|
77
78
|
```
|
|
78
79
|
|
|
80
|
+
### Using `.wardignore`
|
|
81
|
+
|
|
82
|
+
You can also create a standard `.wardignore` file in your project root (works like `.gitignore`):
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
secrets.txt
|
|
86
|
+
generated/
|
|
87
|
+
*.log
|
|
88
|
+
```
|
|
89
|
+
|
|
79
90
|
## License
|
|
80
91
|
|
|
81
92
|
MIT
|
package/package.json
CHANGED
package/src/config.rs
CHANGED
|
@@ -37,12 +37,24 @@ impl Default for Config {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
pub fn load_config() -> Result<Config> {
|
|
40
|
-
let
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
let config: Config = toml::from_str(&content).context("Failed to parse ward.toml")?;
|
|
44
|
-
Ok(config)
|
|
40
|
+
let mut config = if Path::new("ward.toml").exists() {
|
|
41
|
+
let content = fs::read_to_string("ward.toml").context("Failed to read ward.toml")?;
|
|
42
|
+
toml::from_str(&content).context("Failed to parse ward.toml")?
|
|
45
43
|
} else {
|
|
46
|
-
|
|
44
|
+
Config::default()
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Load .wardignore if exists
|
|
48
|
+
let ignore_path = Path::new(".wardignore");
|
|
49
|
+
if ignore_path.exists() {
|
|
50
|
+
let content = fs::read_to_string(ignore_path).context("Failed to read .wardignore")?;
|
|
51
|
+
for line in content.lines() {
|
|
52
|
+
let line = line.trim();
|
|
53
|
+
if !line.is_empty() && !line.starts_with('#') {
|
|
54
|
+
config.exclude.push(line.to_string());
|
|
55
|
+
}
|
|
56
|
+
}
|
|
47
57
|
}
|
|
58
|
+
|
|
59
|
+
Ok(config)
|
|
48
60
|
}
|
package/src/git.rs
CHANGED
|
@@ -94,3 +94,26 @@ pub fn get_staged_files() -> Result<Vec<PathBuf>> {
|
|
|
94
94
|
|
|
95
95
|
Ok(files)
|
|
96
96
|
}
|
|
97
|
+
|
|
98
|
+
pub fn get_staged_content(path: &Path) -> Result<String> {
|
|
99
|
+
// Use git show :path/to/file to get content from index
|
|
100
|
+
// Note: path must be relative to repo root, which get_staged_files returns
|
|
101
|
+
let path_str = path.to_str().context("Invalid path encoding")?;
|
|
102
|
+
|
|
103
|
+
let output = Command::new("git")
|
|
104
|
+
.args(&["show", &format!(":{}", path_str)])
|
|
105
|
+
.output()
|
|
106
|
+
.context(format!("Failed to read staged content for {}", path_str))?;
|
|
107
|
+
|
|
108
|
+
if !output.status.success() {
|
|
109
|
+
// If file is deleted in index or error, we might get here.
|
|
110
|
+
// For pre-commit diff-filter=ACM, it should exist.
|
|
111
|
+
// Fallback or error? Let's error for now to be safe.
|
|
112
|
+
return Err(anyhow::anyhow!("git show failed for {}", path_str));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let content = String::from_utf8(output.stdout)
|
|
116
|
+
.context("File content is not valid UTF-8 (binary?)")?;
|
|
117
|
+
|
|
118
|
+
Ok(content)
|
|
119
|
+
}
|
package/src/main.rs
CHANGED
|
@@ -51,9 +51,19 @@ fn main() -> Result<()> {
|
|
|
51
51
|
let mut all_violations = vec![];
|
|
52
52
|
|
|
53
53
|
for file in files {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
// Fetch content from git index (staged)
|
|
55
|
+
match git::get_staged_content(&file) {
|
|
56
|
+
Ok(content) => {
|
|
57
|
+
match scanner.scan_content(&file, &content) {
|
|
58
|
+
Ok(mut v) => all_violations.append(&mut v),
|
|
59
|
+
Err(e) => eprintln!("Error scanning {:?}: {}", file, e),
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
Err(_) => {
|
|
63
|
+
// Binary file or deleted? specific patterns might cause this.
|
|
64
|
+
// For now we skip, mirroring "safe defaults"
|
|
65
|
+
// In future: verbose log
|
|
66
|
+
}
|
|
57
67
|
}
|
|
58
68
|
}
|
|
59
69
|
|
package/src/scanner.rs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
use crate::config::Config;
|
|
2
2
|
use regex::Regex;
|
|
3
3
|
use globset::{Glob, GlobSet, GlobSetBuilder};
|
|
4
|
-
use std::fs;
|
|
4
|
+
// use std::fs; // Removed unused import
|
|
5
5
|
use std::path::{Path, PathBuf};
|
|
6
6
|
use std::collections::HashMap;
|
|
7
7
|
use anyhow::Result;
|
|
@@ -61,19 +61,31 @@ impl Scanner {
|
|
|
61
61
|
Self { config, patterns, exclude_globnet }
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
pub fn
|
|
64
|
+
pub fn scan_content(&self, path: &Path, content: &str) -> Result<Vec<Violation>> {
|
|
65
65
|
if self.exclude_globnet.is_match(path) {
|
|
66
66
|
return Ok(vec![]);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
let mut violations = vec![];
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
|
|
71
|
+
// 0. File Name Check
|
|
72
|
+
if let Some(filename) = path.file_name().and_then(|s| s.to_str()) {
|
|
73
|
+
// Block .env and .env.* (e.g. .env.local, .env.production)
|
|
74
|
+
// But allow .env.example, .env.sample (common safe patterns)
|
|
75
|
+
if filename.starts_with(".env") {
|
|
76
|
+
let is_safe_example = filename.ends_with(".example") || filename.ends_with(".sample");
|
|
77
|
+
|
|
78
|
+
if !is_safe_example {
|
|
79
|
+
violations.push(Violation {
|
|
80
|
+
file: path.to_path_buf(),
|
|
81
|
+
line: 1,
|
|
82
|
+
rule: format!("Critical: {} detected", filename),
|
|
83
|
+
snippet: "Do not commit .env files. Use .env.example instead.".to_string(),
|
|
84
|
+
});
|
|
85
|
+
return Ok(violations);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
77
89
|
|
|
78
90
|
for (i, line) in content.lines().enumerate() {
|
|
79
91
|
let line_idx = i + 1;
|