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.
- stone_sec-1.0.0/PKG-INFO +6 -0
- stone_sec-1.0.0/README.md +48 -0
- stone_sec-1.0.0/pyproject.toml +14 -0
- stone_sec-1.0.0/setup.cfg +4 -0
- stone_sec-1.0.0/stone_sec/__init__.py +0 -0
- stone_sec-1.0.0/stone_sec/ai_test.py +4 -0
- stone_sec-1.0.0/stone_sec/cli.py +177 -0
- stone_sec-1.0.0/stone_sec/engine/parser.py +19 -0
- stone_sec-1.0.0/stone_sec/engine/rules/__init__.py +0 -0
- stone_sec-1.0.0/stone_sec/engine/rules/eval_rule.py +36 -0
- stone_sec-1.0.0/stone_sec/engine/rules/os_system_rule.py +39 -0
- stone_sec-1.0.0/stone_sec/engine/rules/runner.py +26 -0
- stone_sec-1.0.0/stone_sec/engine/rules/subprocess_shell_rule.py +38 -0
- stone_sec-1.0.0/stone_sec/engine/scanner.py +35 -0
- stone_sec-1.0.0/stone_sec/engine/severity.py +18 -0
- stone_sec-1.0.0/stone_sec/llm/__init__.py +0 -0
- stone_sec-1.0.0/stone_sec/llm/base.py +14 -0
- stone_sec-1.0.0/stone_sec/llm/ollama_provider.py +37 -0
- stone_sec-1.0.0/stone_sec/llm/prompt.py +27 -0
- stone_sec-1.0.0/stone_sec/models/__init__.py +0 -0
- stone_sec-1.0.0/stone_sec/models/finding.py +19 -0
- stone_sec-1.0.0/stone_sec/output/json_formatter.py +30 -0
- stone_sec-1.0.0/stone_sec/test_file.py +6 -0
- stone_sec-1.0.0/stone_sec.egg-info/PKG-INFO +6 -0
- stone_sec-1.0.0/stone_sec.egg-info/SOURCES.txt +26 -0
- stone_sec-1.0.0/stone_sec.egg-info/dependency_links.txt +1 -0
- stone_sec-1.0.0/stone_sec.egg-info/entry_points.txt +2 -0
- stone_sec-1.0.0/stone_sec.egg-info/top_level.txt +1 -0
stone_sec-1.0.0/PKG-INFO
ADDED
|
@@ -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"
|
|
File without changes
|
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
stone_sec
|