diff --git a/package-lock.json b/package-lock.json index 81f52c8..fe06deb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "contract-guard", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "contract-guard", - "version": "1.2.0", + "version": "1.3.0", "license": "Apache-2.0", "devDependencies": { "@types/node": "^20.16.1", diff --git a/package.json b/package.json index a1b048d..66b32e5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "contract-guard", "displayName": "contract-guard", "description": "A VS Code extension that finds security issues in code, configs, queries, Dockerfiles, and secrets.", - "version": "1.2.1", + "version": "1.3.0", "publisher": "BlackplaneSystems", "license": "Apache-2.0", "icon": "media/icon.png", @@ -164,7 +164,7 @@ }, "scripts": { "build": "tsc -p ./tsconfig.json", - "package": "node -e \"require('fs').mkdirSync('dist-vsix',{recursive:true})\" && vsce package --out dist-vsix/contractguard-1.2.1.vsix", + "package": "node -e \"require('fs').mkdirSync('dist-vsix',{recursive:true})\" && vsce package --out dist-vsix/contractguard-1.3.0.vsix", "prepackage": "npm run build" }, "devDependencies": { diff --git a/pyproject.toml b/pyproject.toml index d7b2f2e..b1d5504 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "contractguard" -version = "1.2.0" +version = "1.3.0" description = "ContractGuard security analysis core for VS Code and CI workflows." readme = "README.md" license = {text = "Apache-2.0"} diff --git a/src/contractguard/__init__.py b/src/contractguard/__init__.py index 0f9bd50..842e375 100644 --- a/src/contractguard/__init__.py +++ b/src/contractguard/__init__.py @@ -1,3 +1,3 @@ """ContractGuard core package.""" -__version__ = "1.2.0" +__version__ = "1.3.0" diff --git a/src/contractguard/analyzers/file_filters.py b/src/contractguard/analyzers/file_filters.py new file mode 100644 index 0000000..f82de40 --- /dev/null +++ b/src/contractguard/analyzers/file_filters.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from pathlib import Path + +_SKIP_DIRS = { + ".git", + ".hg", + ".svn", + ".tox", + ".venv", + "venv", + "node_modules", + "dist", + "dist-vsix", + "build", + "out", + ".pytest_cache", + "__pycache__", + ".mypy_cache", + ".ruff_cache", +} + + +def should_skip_path(path: Path) -> bool: + return any(part in _SKIP_DIRS for part in path.parts) diff --git a/src/contractguard/analyzers/pii_analyzer.py b/src/contractguard/analyzers/pii_analyzer.py index cd951ac..7623ef9 100644 --- a/src/contractguard/analyzers/pii_analyzer.py +++ b/src/contractguard/analyzers/pii_analyzer.py @@ -8,12 +8,14 @@ from __future__ import annotations +import ipaddress import json import re from pathlib import Path from typing import Any from contractguard.engine import Finding, Severity, load_rules_for_analyzer, run_rules +from contractguard.analyzers.file_filters import should_skip_path _PII_PATTERNS: list[tuple[str, re.Pattern, str]] = [ ("ssn", re.compile(r"\b\d{3}-\d{2}-\d{4}\b"), "Social Security Number"), @@ -65,8 +67,10 @@ def extract_facts(content: str, filename: str = "") -> dict[str, Any]: for line_num, line in enumerate(content.splitlines(), 1): for pii_name, regex, desc in _PII_PATTERNS: for match in regex.finditer(line): - facts["pii_count"] += 1 matched = match.group(0) + if pii_name == "ip_address" and _is_non_personal_ip(matched): + continue + facts["pii_count"] += 1 if len(matched) > 8: preview = matched[:3] + "***" + matched[-2:] else: @@ -106,12 +110,14 @@ def load_files(path: str | Path) -> list[tuple[str, str]]: if path.is_dir(): for f in sorted(path.rglob("*")): - if f.is_file() and f.suffix.lower() not in _skip: + if f.is_file() and f.suffix.lower() not in _skip and not should_skip_path(f): try: files.append((str(f), f.read_text(encoding="utf-8", errors="replace"))) except Exception: continue elif path.is_file(): + if should_skip_path(path): + return files try: files.append((str(path), path.read_text(encoding="utf-8", errors="replace"))) except Exception: @@ -119,6 +125,14 @@ def load_files(path: str | Path) -> list[tuple[str, str]]: return files +def _is_non_personal_ip(value: str) -> bool: + try: + ip_value = ipaddress.ip_address(value) + except ValueError: + return False + return ip_value.is_loopback or ip_value.is_unspecified or ip_value.is_reserved + + def analyze(path: str | Path, rules_dir: str | Path) -> list[Finding]: """Run PII detection on files at *path*.""" files = load_files(path) diff --git a/src/contractguard/analyzers/secrets_analyzer.py b/src/contractguard/analyzers/secrets_analyzer.py index 627ddd7..60eb4b4 100644 --- a/src/contractguard/analyzers/secrets_analyzer.py +++ b/src/contractguard/analyzers/secrets_analyzer.py @@ -11,6 +11,7 @@ from typing import Any from contractguard.engine import Finding, Severity, load_rules_for_analyzer, run_rules +from contractguard.analyzers.file_filters import should_skip_path _SECRET_PATTERNS: list[tuple[str, re.Pattern, str]] = [ ("aws_access_key", re.compile(r"(?:^|[^A-Za-z0-9/+=])(?:AKIA[0-9A-Z]{16})(?:[^A-Za-z0-9/+=]|$)"), "block"), @@ -115,13 +116,15 @@ def load_files(path: str | Path) -> list[tuple[str, str]]: if path.is_dir(): for f in sorted(path.rglob("*")): - if f.is_file() and f.suffix.lower() not in _SKIP_EXTENSIONS: + if f.is_file() and f.suffix.lower() not in _SKIP_EXTENSIONS and not should_skip_path(f): try: content = f.read_text(encoding="utf-8", errors="replace") files.append((str(f), content)) except Exception: continue elif path.is_file(): + if should_skip_path(path): + return files try: content = path.read_text(encoding="utf-8", errors="replace") files.append((str(path), content))