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 CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "ward"
3
- version = "0.1.0"
3
+ version = "0.1.3"
4
4
  edition = "2021"
5
5
  authors = ["awixor"]
6
6
  description = "Local-First Git Guard for preventing secret leaks"
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Elhoucine Aouassar
3
+ Copyright (c) 2026 awixor
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -4,14 +4,15 @@
4
4
 
5
5
  ## 🚀 Features
6
6
 
7
- - **Zero Friction:** Scans staged files in < 50ms.
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 or patterns via `ward.toml`.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-ward",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Local-First Git Guard for preventing secret leaks",
5
5
  "bin": {
6
6
  "ward": "./bin/ward.js"
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 config_path = Path::new("ward.toml");
41
- if config_path.exists() {
42
- let content = fs::read_to_string(config_path).context("Failed to read ward.toml")?;
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
- Ok(Config::default())
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
- match scanner.scan_file(&file) {
55
- Ok(mut v) => all_violations.append(&mut v),
56
- Err(e) => eprintln!("Error scanning {:?}: {}", file, e),
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 scan_file(&self, path: &Path) -> Result<Vec<Violation>> {
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
- // Skip binary check (basic)
72
- // In a real impl, we'd read first 1024 bytes to check for nulls
73
- let content = match fs::read_to_string(path) {
74
- Ok(c) => c,
75
- Err(_) => return Ok(vec![]), // Skip binary or unreadable
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;