stone-sec 1.0.0__tar.gz

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.
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: stone-sec
3
+ Version: 1.0.0
4
+ Summary: Local-first deterministic AI-enhanced security code review CLI for Python.
5
+ Author: Your Name
6
+ Requires-Python: >=3.11
@@ -0,0 +1,48 @@
1
+ # Stone-Sec
2
+
3
+ Local-first, CI-safe Python security scanner with optional AI explanations.
4
+
5
+ ## Why Stone-Sec
6
+ - Deterministic static analysis
7
+ - Predictable CI behavior
8
+ - No cloud dependency
9
+ - AI used only for explanation, never decisions
10
+
11
+ ## Installation
12
+ pip install -e .
13
+
14
+ ## Basic Usage
15
+ stone-sec review path/
16
+
17
+ ## CI Enforcement
18
+ stone-sec review path/ --fail-on high
19
+
20
+ ## JSON Output
21
+ stone-sec review path/ --format json
22
+
23
+ ## AI Explanations (Optional)
24
+ stone-sec review path/ --provider ollama
25
+
26
+ ## GitHub Actions (CI)
27
+
28
+ Run Stone-Sec automatically in GitHub Actions to enforce security checks.
29
+
30
+ ```yaml
31
+ - name: Run Stone-Sec
32
+ uses: DaniOps/stone-sec@v1
33
+ with:
34
+ path: .
35
+ fail_on: high
36
+ ```
37
+
38
+ ## Environment Check
39
+ stone-sec doctor
40
+
41
+ ## Design Philosophy
42
+ - Detection is deterministic
43
+ - AI is enhancement-only
44
+ - Exit codes drive CI
45
+ - Local-first by default
46
+
47
+ ## License
48
+ MIT
@@ -0,0 +1,14 @@
1
+ [project]
2
+ name = "stone-sec"
3
+ version = "1.0.0"
4
+ description = "Local-first deterministic AI-enhanced security code review CLI for Python."
5
+ authors = [{ name="Your Name" }]
6
+ requires-python = ">=3.11"
7
+ dependencies = []
8
+
9
+ [project.scripts]
10
+ stone-sec = "stone_sec.cli:main"
11
+
12
+ [build-system]
13
+ requires = ["setuptools>=61.0"]
14
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,4 @@
1
+ import os
2
+
3
+ user_input = input("cmd: ")
4
+ os.system(user_input)
@@ -0,0 +1,177 @@
1
+ from stone_sec.llm.ollama_provider import OllamaProvider
2
+ from stone_sec.llm.prompt import build_prompt
3
+ from stone_sec.engine.rules.runner import run_rules
4
+ from stone_sec.engine.parser import parse_python_file
5
+ from stone_sec.engine.rules.eval_rule import EvalUsageRule
6
+ from stone_sec.engine.scanner import discover_python_files
7
+ import argparse
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+
13
+ def create_parser() -> argparse.ArgumentParser:
14
+ parser = argparse.ArgumentParser(
15
+ prog="stone-sec",
16
+ description="Local-first deterministic security code review CLI for Python."
17
+ )
18
+
19
+ subparsers = parser.add_subparsers(dest="command")
20
+
21
+ # Review command
22
+ review_parser = subparsers.add_parser(
23
+ "review",
24
+ help="Scan Python files for security issues."
25
+ )
26
+
27
+ review_parser.add_argument(
28
+ "path",
29
+ type=str,
30
+ help="Path to Python file or directory to scan."
31
+ )
32
+
33
+ review_parser.add_argument(
34
+ "--format",
35
+ choices=["text", "json"],
36
+ default="text",
37
+ help="Output format (text or json)",
38
+ )
39
+
40
+ review_parser.add_argument(
41
+ "--fail-on",
42
+ type=str,
43
+ choices=["low", "medium", "high", "critical"],
44
+ help="Fail with exit code 1 if findings meet or exceed this severity."
45
+ )
46
+
47
+ review_parser.add_argument(
48
+ "--provider",
49
+ choices=["ollama"],
50
+ help="LLM provider for enhanced explanations",
51
+ )
52
+
53
+ # Version command
54
+ subparsers.add_parser(
55
+ "version",
56
+ help="Show tool version."
57
+ )
58
+
59
+ return parser
60
+
61
+
62
+ def handle_review(args):
63
+ import sys
64
+ from pathlib import Path
65
+
66
+ from stone_sec.engine.severity import Severity
67
+ from stone_sec.engine.scanner import discover_python_files
68
+ from stone_sec.engine.parser import parse_python_file
69
+ from stone_sec.engine.rules.runner import run_rules
70
+ from stone_sec.llm.ollama_provider import OllamaProvider
71
+ from stone_sec.llm.prompt import build_prompt
72
+ from stone_sec.output.json_formatter import findings_to_json
73
+
74
+ target_path = Path(args.path)
75
+
76
+ if not target_path.exists():
77
+ print(f"[ERROR] Path does not exist: {target_path}")
78
+ sys.exit(1)
79
+
80
+ python_files = discover_python_files(target_path)
81
+
82
+ if not python_files:
83
+ if args.format == "json":
84
+ print(findings_to_json([]))
85
+ else:
86
+ print("No Python files found.")
87
+ sys.exit(0)
88
+
89
+ findings = []
90
+
91
+ # --- Deterministic detection phase ---
92
+ for file_path in python_files:
93
+ tree = parse_python_file(file_path)
94
+ if tree is None:
95
+ continue
96
+
97
+ findings.extend(run_rules(tree, file_path))
98
+
99
+ if not findings:
100
+ if args.format == "json":
101
+ print(findings_to_json([]))
102
+ else:
103
+ print("No security issues found.")
104
+ sys.exit(0)
105
+
106
+ # --- Optional LLM enhancement (never affects severity/exit) ---
107
+ provider = None
108
+ if getattr(args, "provider", None) == "ollama":
109
+ provider = OllamaProvider()
110
+
111
+ if provider:
112
+ for f in findings:
113
+ prompt = build_prompt(f)
114
+ result = provider.generate(prompt)
115
+
116
+ f.explanation = result.get("explanation")
117
+ f.exploit_scenario = result.get("exploit_scenario")
118
+ f.remediation = result.get("remediation")
119
+
120
+ # --- Output phase ---
121
+ if args.format == "json":
122
+ print(findings_to_json(findings))
123
+ else:
124
+ print(f"Found {len(findings)} issue(s):\n")
125
+
126
+ for f in findings:
127
+ print(f"[{str(f.severity)}] {f.title}")
128
+ print(f"Rule: {f.rule_id}")
129
+ print(f"File: {f.file}")
130
+ print(f"Line: {f.line}")
131
+ print(f"Snippet: {f.snippet}")
132
+
133
+ if f.explanation:
134
+ print(f"Explanation: {f.explanation}")
135
+ if f.exploit_scenario:
136
+ print(f"Exploit: {f.exploit_scenario}")
137
+ if f.remediation:
138
+ print(f"Fix: {f.remediation}")
139
+
140
+ print()
141
+
142
+ # --- CI fail-on logic (deterministic, unaffected by LLM/output) ---
143
+ highest_severity = None
144
+ for f in findings:
145
+ if highest_severity is None or f.severity.value > highest_severity.value:
146
+ highest_severity = f.severity
147
+
148
+ if args.fail_on:
149
+ threshold = Severity.from_string(args.fail_on)
150
+ if highest_severity and highest_severity.value >= threshold.value:
151
+ sys.exit(1)
152
+
153
+ sys.exit(0)
154
+
155
+
156
+ def handle_version():
157
+ print("stone-sec version 0.1.0")
158
+ sys.exit(0)
159
+
160
+
161
+ def main():
162
+ parser = create_parser()
163
+ args = parser.parse_args()
164
+
165
+ if not args.command:
166
+ parser.print_help()
167
+ sys.exit(0)
168
+
169
+ if args.command == "review":
170
+ handle_review(args)
171
+
172
+ elif args.command == "version":
173
+ handle_version()
174
+
175
+ else:
176
+ parser.print_help()
177
+ sys.exit(1)
@@ -0,0 +1,19 @@
1
+ import ast
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+
6
+ def parse_python_file(path: Path) -> Optional[ast.AST]:
7
+ """
8
+ Safely parse a Python file into an AST.
9
+
10
+ Returns:
11
+ ast.AST if parsing succeeds
12
+ None if file contains syntax errors or cannot be read
13
+ """
14
+ try:
15
+ source = path.read_text(encoding="utf-8")
16
+ return ast.parse(source, filename=str(path))
17
+ except (SyntaxError, UnicodeDecodeError, OSError):
18
+ # We never crash on bad files
19
+ return None
File without changes
@@ -0,0 +1,36 @@
1
+ import ast
2
+ from pathlib import Path
3
+ from typing import List
4
+
5
+ from stone_sec.engine.severity import Severity
6
+ from stone_sec.models.finding import Finding
7
+
8
+
9
+ class EvalUsageRule(ast.NodeVisitor):
10
+ """
11
+ Detects usage of eval().
12
+ """
13
+
14
+ RULE_ID = "PY-EVAL-001"
15
+
16
+ def __init__(self, file_path: Path):
17
+ self.file_path = file_path
18
+ self.findings: List[Finding] = []
19
+
20
+ def visit_Call(self, node: ast.Call):
21
+ # Check if function name is `eval`
22
+ if isinstance(node.func, ast.Name) and node.func.id == "eval":
23
+ snippet = "eval(...)"
24
+
25
+ self.findings.append(
26
+ Finding(
27
+ file=self.file_path,
28
+ line=node.lineno,
29
+ rule_id=self.RULE_ID,
30
+ severity=Severity.HIGH,
31
+ title="Use of eval()",
32
+ snippet=snippet,
33
+ )
34
+ )
35
+
36
+ self.generic_visit(node)
@@ -0,0 +1,39 @@
1
+ import ast
2
+ from pathlib import Path
3
+ from typing import List
4
+
5
+ from stone_sec.engine.severity import Severity
6
+ from stone_sec.models.finding import Finding
7
+
8
+
9
+ class OsSystemRule(ast.NodeVisitor):
10
+ """
11
+ Detects usage of os.system().
12
+ """
13
+
14
+ RULE_ID = "PY-OS-SYSTEM-001"
15
+
16
+ def __init__(self, file_path: Path):
17
+ self.file_path = file_path
18
+ self.findings: List[Finding] = []
19
+
20
+ def visit_Call(self, node: ast.Call):
21
+ # Detect os.system(...)
22
+ if (
23
+ isinstance(node.func, ast.Attribute)
24
+ and isinstance(node.func.value, ast.Name)
25
+ and node.func.value.id == "os"
26
+ and node.func.attr == "system"
27
+ ):
28
+ self.findings.append(
29
+ Finding(
30
+ file=self.file_path,
31
+ line=node.lineno,
32
+ rule_id=self.RULE_ID,
33
+ severity=Severity.HIGH,
34
+ title="Use of os.system()",
35
+ snippet="os.system(...)",
36
+ )
37
+ )
38
+
39
+ self.generic_visit(node)
@@ -0,0 +1,26 @@
1
+ from pathlib import Path
2
+ from typing import List, Type
3
+ import ast
4
+
5
+ from stone_sec.models.finding import Finding
6
+ from stone_sec.engine.rules.eval_rule import EvalUsageRule
7
+ from stone_sec.engine.rules.os_system_rule import OsSystemRule
8
+ from stone_sec.engine.rules.subprocess_shell_rule import SubprocessShellRule
9
+
10
+
11
+ RULES: List[Type[ast.NodeVisitor]] = [
12
+ EvalUsageRule,
13
+ OsSystemRule,
14
+ SubprocessShellRule,
15
+ ]
16
+
17
+
18
+ def run_rules(tree: ast.AST, file_path: Path) -> List[Finding]:
19
+ findings: List[Finding] = []
20
+
21
+ for rule_cls in RULES:
22
+ rule = rule_cls(file_path)
23
+ rule.visit(tree)
24
+ findings.extend(rule.findings)
25
+
26
+ return findings
@@ -0,0 +1,38 @@
1
+ import ast
2
+ from pathlib import Path
3
+ from typing import List
4
+
5
+ from stone_sec.engine.severity import Severity
6
+ from stone_sec.models.finding import Finding
7
+
8
+
9
+ class SubprocessShellRule(ast.NodeVisitor):
10
+ """
11
+ Detects subprocess calls with shell=True.
12
+ """
13
+
14
+ RULE_ID = "PY-SUBPROCESS-001"
15
+
16
+ def __init__(self, file_path: Path):
17
+ self.file_path = file_path
18
+ self.findings: List[Finding] = []
19
+
20
+ def visit_Call(self, node: ast.Call):
21
+ # Look for subprocess.* calls
22
+ if isinstance(node.func, ast.Attribute):
23
+ for keyword in node.keywords:
24
+ if keyword.arg == "shell" and isinstance(keyword.value, ast.Constant):
25
+ if keyword.value.value is True:
26
+ self.findings.append(
27
+ Finding(
28
+ file=self.file_path,
29
+ line=node.lineno,
30
+ rule_id=self.RULE_ID,
31
+ severity=Severity.HIGH,
32
+ title="subprocess call with shell=True",
33
+ snippet="subprocess(..., shell=True)",
34
+ )
35
+ )
36
+ break
37
+
38
+ self.generic_visit(node)
@@ -0,0 +1,35 @@
1
+ from pathlib import Path
2
+ from typing import List
3
+
4
+ EXCLUDED_DIRS = {
5
+ ".venv",
6
+ "venv",
7
+ "__pycache__",
8
+ "site-packages",
9
+ }
10
+
11
+
12
+ def discover_python_files(target: Path) -> List[Path]:
13
+ """
14
+ Discover Python files from a file or directory path,
15
+ excluding virtual environments and dependencies.
16
+ """
17
+
18
+ python_files: List[Path] = []
19
+
20
+ if target.is_file():
21
+ if target.suffix == ".py":
22
+ return [target.resolve()]
23
+ return []
24
+
25
+ if target.is_dir():
26
+ for path in target.rglob("*.py"):
27
+ # Skip excluded directories
28
+ if any(part in EXCLUDED_DIRS for part in path.parts):
29
+ continue
30
+
31
+ if path.is_file():
32
+ python_files.append(path.resolve())
33
+
34
+ python_files.sort()
35
+ return python_files
@@ -0,0 +1,18 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Severity(Enum):
5
+ LOW = 1
6
+ MEDIUM = 2
7
+ HIGH = 3
8
+ CRITICAL = 4
9
+
10
+ def __str__(self) -> str:
11
+ return self.name.lower()
12
+
13
+ @classmethod
14
+ def from_string(cls, value: str) -> "Severity":
15
+ try:
16
+ return cls[value.upper()]
17
+ except KeyError:
18
+ raise ValueError(f"Invalid severity level: {value}")
File without changes
@@ -0,0 +1,14 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict
3
+
4
+
5
+ class LLMProvider(ABC):
6
+ @abstractmethod
7
+ def generate(self, prompt: str) -> Dict[str, str]:
8
+ """
9
+ Must return a dict with keys:
10
+ - explanation
11
+ - exploit_scenario
12
+ - remediation
13
+ """
14
+ raise NotImplementedError
@@ -0,0 +1,37 @@
1
+ import json
2
+ import subprocess
3
+ from typing import Dict
4
+
5
+ from stone_sec.llm.base import LLMProvider
6
+
7
+
8
+ class OllamaProvider(LLMProvider):
9
+ def __init__(self, model: str = "llama3"):
10
+ self.model = model
11
+
12
+ def generate(self, prompt: str) -> Dict[str, str]:
13
+ try:
14
+ proc = subprocess.run(
15
+ ["ollama", "run", self.model],
16
+ input=prompt,
17
+ capture_output=True,
18
+ text=True,
19
+ encoding="utf-8",
20
+ errors="ignore",
21
+ timeout=120,
22
+ )
23
+ output = proc.stdout.strip()
24
+ data = json.loads(output)
25
+
26
+ return {
27
+ "explanation": data.get("explanation", ""),
28
+ "exploit_scenario": data.get("exploit_scenario", ""),
29
+ "remediation": data.get("remediation", ""),
30
+ }
31
+ except Exception:
32
+ # Never crash — fallback
33
+ return {
34
+ "explanation": "Potential security risk detected.",
35
+ "exploit_scenario": "An attacker could abuse this behavior if input is controlled.",
36
+ "remediation": "Avoid unsafe constructs and validate inputs.",
37
+ }
@@ -0,0 +1,27 @@
1
+ from stone_sec.models.finding import Finding
2
+
3
+
4
+ def build_prompt(finding: Finding) -> str:
5
+ """
6
+ Build a strict JSON-only prompt for the LLM.
7
+ """
8
+ return f"""
9
+ You are a security analysis engine.
10
+
11
+ Analyze the following security finding and return ONLY valid JSON with keys:
12
+ - explanation
13
+ - exploit_scenario
14
+ - remediation
15
+
16
+ Finding details:
17
+ - Title: {finding.title}
18
+ - Severity: {finding.severity}
19
+ - File: {finding.file}
20
+ - Line: {finding.line}
21
+ - Code Snippet: {finding.snippet}
22
+
23
+ Rules:
24
+ - Do not include any text outside JSON
25
+ - Do not change severity
26
+ - Do not invent vulnerabilities
27
+ """
File without changes
@@ -0,0 +1,19 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ from stone_sec.engine.severity import Severity
6
+
7
+
8
+ @dataclass
9
+ class Finding:
10
+ file: Path
11
+ line: int
12
+ rule_id: str
13
+ severity: Severity
14
+ title: str
15
+ snippet: str
16
+
17
+ explanation: Optional[str] = None
18
+ exploit_scenario: Optional[str] = None
19
+ remediation: Optional[str] = None
@@ -0,0 +1,30 @@
1
+ import json
2
+ from typing import List
3
+ from stone_sec.models.finding import Finding
4
+
5
+
6
+ def findings_to_json(findings: List[Finding]) -> str:
7
+ data = []
8
+
9
+ for f in findings:
10
+ data.append(
11
+ {
12
+ "rule_id": f.rule_id,
13
+ "severity": str(f.severity),
14
+ "title": f.title,
15
+ "file": str(f.file),
16
+ "line": f.line,
17
+ "snippet": f.snippet,
18
+ "explanation": f.explanation,
19
+ "exploit_scenario": f.exploit_scenario,
20
+ "remediation": f.remediation,
21
+ }
22
+ )
23
+
24
+ return json.dumps(
25
+ {
26
+ "total_findings": len(findings),
27
+ "findings": data,
28
+ },
29
+ indent=2,
30
+ )
@@ -0,0 +1,6 @@
1
+ import os
2
+ import subprocess
3
+
4
+ os.system("ls")
5
+ subprocess.run("ls", shell=True)
6
+ eval("2 + 2")
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: stone-sec
3
+ Version: 1.0.0
4
+ Summary: Local-first deterministic AI-enhanced security code review CLI for Python.
5
+ Author: Your Name
6
+ Requires-Python: >=3.11
@@ -0,0 +1,26 @@
1
+ README.md
2
+ pyproject.toml
3
+ stone_sec/__init__.py
4
+ stone_sec/ai_test.py
5
+ stone_sec/cli.py
6
+ stone_sec/test_file.py
7
+ stone_sec.egg-info/PKG-INFO
8
+ stone_sec.egg-info/SOURCES.txt
9
+ stone_sec.egg-info/dependency_links.txt
10
+ stone_sec.egg-info/entry_points.txt
11
+ stone_sec.egg-info/top_level.txt
12
+ stone_sec/engine/parser.py
13
+ stone_sec/engine/scanner.py
14
+ stone_sec/engine/severity.py
15
+ stone_sec/engine/rules/__init__.py
16
+ stone_sec/engine/rules/eval_rule.py
17
+ stone_sec/engine/rules/os_system_rule.py
18
+ stone_sec/engine/rules/runner.py
19
+ stone_sec/engine/rules/subprocess_shell_rule.py
20
+ stone_sec/llm/__init__.py
21
+ stone_sec/llm/base.py
22
+ stone_sec/llm/ollama_provider.py
23
+ stone_sec/llm/prompt.py
24
+ stone_sec/models/__init__.py
25
+ stone_sec/models/finding.py
26
+ stone_sec/output/json_formatter.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ stone-sec = stone_sec.cli:main
@@ -0,0 +1 @@
1
+ stone_sec