refutescan 0.1.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.
- refutescan-0.1.0/LICENSE +21 -0
- refutescan-0.1.0/PKG-INFO +132 -0
- refutescan-0.1.0/README.md +98 -0
- refutescan-0.1.0/pyproject.toml +69 -0
- refutescan-0.1.0/setup.cfg +4 -0
- refutescan-0.1.0/src/refutescan/__init__.py +38 -0
- refutescan-0.1.0/src/refutescan/adapters/__init__.py +11 -0
- refutescan-0.1.0/src/refutescan/adapters/openai_chat.py +51 -0
- refutescan-0.1.0/src/refutescan/cli.py +116 -0
- refutescan-0.1.0/src/refutescan/codemap.py +84 -0
- refutescan-0.1.0/src/refutescan/fileaccess.py +142 -0
- refutescan-0.1.0/src/refutescan/models.py +91 -0
- refutescan-0.1.0/src/refutescan/providers.py +32 -0
- refutescan-0.1.0/src/refutescan/safety.py +89 -0
- refutescan-0.1.0/src/refutescan/sandbox/Dockerfile +26 -0
- refutescan-0.1.0/src/refutescan/sandbox/__init__.py +10 -0
- refutescan-0.1.0/src/refutescan/sandbox/build.sh +11 -0
- refutescan-0.1.0/src/refutescan/sandbox/docker.py +146 -0
- refutescan-0.1.0/src/refutescan/sandbox/toolrunner.py +214 -0
- refutescan-0.1.0/src/refutescan/scanner.py +462 -0
- refutescan-0.1.0/src/refutescan/vulns.py +29 -0
- refutescan-0.1.0/src/refutescan.egg-info/PKG-INFO +132 -0
- refutescan-0.1.0/src/refutescan.egg-info/SOURCES.txt +26 -0
- refutescan-0.1.0/src/refutescan.egg-info/dependency_links.txt +1 -0
- refutescan-0.1.0/src/refutescan.egg-info/entry_points.txt +3 -0
- refutescan-0.1.0/src/refutescan.egg-info/requires.txt +11 -0
- refutescan-0.1.0/src/refutescan.egg-info/top_level.txt +1 -0
- refutescan-0.1.0/tests/test_kernel.py +134 -0
refutescan-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vinay Vobbilichetty
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: refutescan
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A two-LLM, refute-first agentic source-code vulnerability scanner — wide-net navigate, skeptical refute, jailed in docker. Bring your own LLM.
|
|
5
|
+
Author-email: Vinay Vobbilichetty <vinayvobbilichetty11@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vinayvobbili/refutescan
|
|
8
|
+
Project-URL: Repository, https://github.com/vinayvobbili/refutescan
|
|
9
|
+
Project-URL: Issues, https://github.com/vinayvobbili/refutescan/issues
|
|
10
|
+
Keywords: security,sast,vulnerability-scanner,appsec,code-security,agentic,llm,ai-security,static-analysis,secure-code-review,false-positive-reduction,supply-chain,sandbox
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Information Technology
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: pydantic>=2
|
|
25
|
+
Requires-Dist: langchain-core>=0.3
|
|
26
|
+
Provides-Extra: openai
|
|
27
|
+
Requires-Dist: langchain-openai>=0.1; extra == "openai"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
31
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
32
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# refutescan
|
|
36
|
+
|
|
37
|
+
**A two-LLM, refute-first agentic source-code vulnerability scanner.**
|
|
38
|
+
|
|
39
|
+
Most LLM code scanners have the same problem: they hallucinate. Point a model at
|
|
40
|
+
a repo and it will confidently report SQL injection in a parameterized query and
|
|
41
|
+
SSRF behind an allowlist. The noise buries the real bugs.
|
|
42
|
+
|
|
43
|
+
refutescan splits the job across two models:
|
|
44
|
+
|
|
45
|
+
1. **Navigate (wide net).** A fast tool-calling model explores the repo with
|
|
46
|
+
read-only tools (`list_dir` / `read_file` / `grep`), traces untrusted input
|
|
47
|
+
toward dangerous sinks, and records *candidate* findings. It's cheap and it
|
|
48
|
+
over-reports on purpose.
|
|
49
|
+
2. **Refute (skeptic).** A stronger model re-reads the actual code slice around
|
|
50
|
+
each candidate from disk — ground truth, not the navigator's paraphrase — and
|
|
51
|
+
is prompted to **refute** it. Sanitized? Parameterized? Gated by auth? Not
|
|
52
|
+
reachable? It's culled, with the reason kept. Only what survives is reported.
|
|
53
|
+
|
|
54
|
+
You get the recall of an agentic scanner without the false-positive flood.
|
|
55
|
+
|
|
56
|
+
Every scan runs **jailed in an ephemeral docker sandbox** by default: git URLs
|
|
57
|
+
are cloned *inside a container* (no host mounts), then audited in a second
|
|
58
|
+
container with **no network, read-only, no capabilities, non-root**, and only
|
|
59
|
+
the repo mounted. A hostile repo (malicious submodule, hook, symlink) touches
|
|
60
|
+
neither your filesystem nor the network.
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
pip install refutescan # core kernel
|
|
66
|
+
pip install 'refutescan[openai]' # + the OpenAI adapter for the CLI
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
For sandboxed scans (recommended), build the jail image once:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
refutescan-build-sandbox
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
(Requires Docker. Without it, refutescan falls back to a guarded in-process scan.)
|
|
76
|
+
|
|
77
|
+
## CLI
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
export OPENAI_API_KEY=sk-...
|
|
81
|
+
refutescan https://github.com/owner/repo
|
|
82
|
+
refutescan ./path/to/local/repo --judge-model gpt-4o --json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Exit code is non-zero when confirmed findings exist, so it drops straight into CI.
|
|
86
|
+
|
|
87
|
+
## Library
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from refutescan import scan, ScanConfig
|
|
91
|
+
from refutescan.adapters import openai_navigator_factory, openai_judge_factory
|
|
92
|
+
|
|
93
|
+
result = scan(
|
|
94
|
+
"https://github.com/owner/repo",
|
|
95
|
+
navigator_factory=openai_navigator_factory("gpt-4o-mini"),
|
|
96
|
+
judge_factory=openai_judge_factory("gpt-4o"),
|
|
97
|
+
config=ScanConfig(sandbox="docker"), # auto | docker | inprocess
|
|
98
|
+
progress=lambda phase: print(phase),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
for f in result.findings:
|
|
102
|
+
print(f["severity"], f["title"], f"{f['file']}:{f['line']}")
|
|
103
|
+
print(" ", f["reasoning"])
|
|
104
|
+
print(" fix:", f["recommendation"])
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`result.culled` holds the refuted candidates with the reason each was rejected —
|
|
108
|
+
useful for tuning and for trusting the tool.
|
|
109
|
+
|
|
110
|
+
## Bring your own model
|
|
111
|
+
|
|
112
|
+
refutescan never imports a provider SDK in its core. Pass any LangChain-style
|
|
113
|
+
chat model through two factories:
|
|
114
|
+
|
|
115
|
+
- `navigator_factory() -> chat_model` — supports `.bind_tools(...)` + `.invoke(...)`
|
|
116
|
+
- `judge_factory() -> judge(prompt, schema) -> instance` — one structured-output call
|
|
117
|
+
|
|
118
|
+
So a local model (vLLM/Ollama via the OpenAI shim, `--base-url`), Anthropic,
|
|
119
|
+
Azure OpenAI, or a corporate gateway all work — wire the factory and go. See
|
|
120
|
+
`refutescan/providers.py`.
|
|
121
|
+
|
|
122
|
+
## What it is and isn't
|
|
123
|
+
|
|
124
|
+
- **Is:** an agentic, read-only, false-positive-resistant first-pass auditor for
|
|
125
|
+
the common web-app vuln classes (injection, authz/IDOR, SSRF, path traversal,
|
|
126
|
+
unsafe deserialization, hardcoded secrets, weak crypto, XSS, auth flaws).
|
|
127
|
+
- **Isn't:** a replacement for a full SAST suite, a proof of exploitability, or a
|
|
128
|
+
patch generator. It finds and explains; a human confirms and fixes.
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# refutescan
|
|
2
|
+
|
|
3
|
+
**A two-LLM, refute-first agentic source-code vulnerability scanner.**
|
|
4
|
+
|
|
5
|
+
Most LLM code scanners have the same problem: they hallucinate. Point a model at
|
|
6
|
+
a repo and it will confidently report SQL injection in a parameterized query and
|
|
7
|
+
SSRF behind an allowlist. The noise buries the real bugs.
|
|
8
|
+
|
|
9
|
+
refutescan splits the job across two models:
|
|
10
|
+
|
|
11
|
+
1. **Navigate (wide net).** A fast tool-calling model explores the repo with
|
|
12
|
+
read-only tools (`list_dir` / `read_file` / `grep`), traces untrusted input
|
|
13
|
+
toward dangerous sinks, and records *candidate* findings. It's cheap and it
|
|
14
|
+
over-reports on purpose.
|
|
15
|
+
2. **Refute (skeptic).** A stronger model re-reads the actual code slice around
|
|
16
|
+
each candidate from disk — ground truth, not the navigator's paraphrase — and
|
|
17
|
+
is prompted to **refute** it. Sanitized? Parameterized? Gated by auth? Not
|
|
18
|
+
reachable? It's culled, with the reason kept. Only what survives is reported.
|
|
19
|
+
|
|
20
|
+
You get the recall of an agentic scanner without the false-positive flood.
|
|
21
|
+
|
|
22
|
+
Every scan runs **jailed in an ephemeral docker sandbox** by default: git URLs
|
|
23
|
+
are cloned *inside a container* (no host mounts), then audited in a second
|
|
24
|
+
container with **no network, read-only, no capabilities, non-root**, and only
|
|
25
|
+
the repo mounted. A hostile repo (malicious submodule, hook, symlink) touches
|
|
26
|
+
neither your filesystem nor the network.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
pip install refutescan # core kernel
|
|
32
|
+
pip install 'refutescan[openai]' # + the OpenAI adapter for the CLI
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For sandboxed scans (recommended), build the jail image once:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
refutescan-build-sandbox
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
(Requires Docker. Without it, refutescan falls back to a guarded in-process scan.)
|
|
42
|
+
|
|
43
|
+
## CLI
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
export OPENAI_API_KEY=sk-...
|
|
47
|
+
refutescan https://github.com/owner/repo
|
|
48
|
+
refutescan ./path/to/local/repo --judge-model gpt-4o --json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Exit code is non-zero when confirmed findings exist, so it drops straight into CI.
|
|
52
|
+
|
|
53
|
+
## Library
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from refutescan import scan, ScanConfig
|
|
57
|
+
from refutescan.adapters import openai_navigator_factory, openai_judge_factory
|
|
58
|
+
|
|
59
|
+
result = scan(
|
|
60
|
+
"https://github.com/owner/repo",
|
|
61
|
+
navigator_factory=openai_navigator_factory("gpt-4o-mini"),
|
|
62
|
+
judge_factory=openai_judge_factory("gpt-4o"),
|
|
63
|
+
config=ScanConfig(sandbox="docker"), # auto | docker | inprocess
|
|
64
|
+
progress=lambda phase: print(phase),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
for f in result.findings:
|
|
68
|
+
print(f["severity"], f["title"], f"{f['file']}:{f['line']}")
|
|
69
|
+
print(" ", f["reasoning"])
|
|
70
|
+
print(" fix:", f["recommendation"])
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`result.culled` holds the refuted candidates with the reason each was rejected —
|
|
74
|
+
useful for tuning and for trusting the tool.
|
|
75
|
+
|
|
76
|
+
## Bring your own model
|
|
77
|
+
|
|
78
|
+
refutescan never imports a provider SDK in its core. Pass any LangChain-style
|
|
79
|
+
chat model through two factories:
|
|
80
|
+
|
|
81
|
+
- `navigator_factory() -> chat_model` — supports `.bind_tools(...)` + `.invoke(...)`
|
|
82
|
+
- `judge_factory() -> judge(prompt, schema) -> instance` — one structured-output call
|
|
83
|
+
|
|
84
|
+
So a local model (vLLM/Ollama via the OpenAI shim, `--base-url`), Anthropic,
|
|
85
|
+
Azure OpenAI, or a corporate gateway all work — wire the factory and go. See
|
|
86
|
+
`refutescan/providers.py`.
|
|
87
|
+
|
|
88
|
+
## What it is and isn't
|
|
89
|
+
|
|
90
|
+
- **Is:** an agentic, read-only, false-positive-resistant first-pass auditor for
|
|
91
|
+
the common web-app vuln classes (injection, authz/IDOR, SSRF, path traversal,
|
|
92
|
+
unsafe deserialization, hardcoded secrets, weak crypto, XSS, auth flaws).
|
|
93
|
+
- **Isn't:** a replacement for a full SAST suite, a proof of exploitability, or a
|
|
94
|
+
patch generator. It finds and explains; a human confirms and fixes.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "refutescan"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A two-LLM, refute-first agentic source-code vulnerability scanner — wide-net navigate, skeptical refute, jailed in docker. Bring your own LLM."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Vinay Vobbilichetty", email = "vinayvobbilichetty11@gmail.com" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"security", "sast", "vulnerability-scanner", "appsec", "code-security",
|
|
15
|
+
"agentic", "llm", "ai-security", "static-analysis", "secure-code-review",
|
|
16
|
+
"false-positive-reduction", "supply-chain", "sandbox",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Intended Audience :: Information Technology",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Topic :: Security",
|
|
28
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
29
|
+
]
|
|
30
|
+
# Core: the kernel + the LangChain interface the navigator/judge speak. Provider
|
|
31
|
+
# SDKs are opt-in extras — bring your own model.
|
|
32
|
+
dependencies = [
|
|
33
|
+
"pydantic>=2",
|
|
34
|
+
"langchain-core>=0.3",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
openai = ["langchain-openai>=0.1"]
|
|
39
|
+
dev = [
|
|
40
|
+
"pytest>=7",
|
|
41
|
+
"ruff>=0.4",
|
|
42
|
+
"build>=1.0",
|
|
43
|
+
"twine>=5.0",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.scripts]
|
|
47
|
+
refutescan = "refutescan.cli:main"
|
|
48
|
+
refutescan-build-sandbox = "refutescan.cli:build_sandbox"
|
|
49
|
+
|
|
50
|
+
[project.urls]
|
|
51
|
+
Homepage = "https://github.com/vinayvobbili/refutescan"
|
|
52
|
+
Repository = "https://github.com/vinayvobbili/refutescan"
|
|
53
|
+
Issues = "https://github.com/vinayvobbili/refutescan/issues"
|
|
54
|
+
|
|
55
|
+
[tool.setuptools.packages.find]
|
|
56
|
+
where = ["src"]
|
|
57
|
+
|
|
58
|
+
[tool.setuptools.package-data]
|
|
59
|
+
# The sandbox image source ships with the package so `refutescan-build-sandbox`
|
|
60
|
+
# can build it from the installed location.
|
|
61
|
+
"refutescan.sandbox" = ["Dockerfile", "build.sh", "toolrunner.py"]
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
testpaths = ["tests"]
|
|
65
|
+
addopts = "-q"
|
|
66
|
+
|
|
67
|
+
[tool.ruff]
|
|
68
|
+
line-length = 100
|
|
69
|
+
target-version = "py310"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""refutescan — a two-LLM, refute-first agentic source-code vulnerability scanner.
|
|
2
|
+
|
|
3
|
+
A fast model NAVIGATES the repo with read-only tools and casts a wide net of
|
|
4
|
+
candidate findings; a stronger model then REFUTES each one against the real code
|
|
5
|
+
slice before it surfaces — so you get the recall of an agentic scanner without
|
|
6
|
+
the false-positive flood. Scans run jailed in an ephemeral docker sandbox
|
|
7
|
+
(clone-in-container, no network, read-only, non-root) by default. Bring your own
|
|
8
|
+
LLMs.
|
|
9
|
+
|
|
10
|
+
from refutescan import scan
|
|
11
|
+
from refutescan.adapters import openai_navigator_factory, openai_judge_factory
|
|
12
|
+
|
|
13
|
+
result = scan(
|
|
14
|
+
"https://github.com/owner/repo",
|
|
15
|
+
navigator_factory=openai_navigator_factory(),
|
|
16
|
+
judge_factory=openai_judge_factory(),
|
|
17
|
+
)
|
|
18
|
+
for f in result.findings:
|
|
19
|
+
print(f["severity"], f["title"], f["file"], f["line"])
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .models import ScanConfig, ScanResult, Verdict
|
|
23
|
+
from .scanner import derive_title, looks_like_git_url, scan
|
|
24
|
+
from .vulns import SEVERITIES, VULN_CLASSES
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"scan",
|
|
30
|
+
"ScanConfig",
|
|
31
|
+
"ScanResult",
|
|
32
|
+
"Verdict",
|
|
33
|
+
"VULN_CLASSES",
|
|
34
|
+
"SEVERITIES",
|
|
35
|
+
"looks_like_git_url",
|
|
36
|
+
"derive_title",
|
|
37
|
+
"__version__",
|
|
38
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Ready-made model factories for common providers.
|
|
2
|
+
|
|
3
|
+
These are thin conveniences over LangChain chat models so you don't have to wire
|
|
4
|
+
the injection seam yourself. Import the one you want; each needs its provider
|
|
5
|
+
extra installed (e.g. ``pip install refutescan[openai]``). Bring your own by
|
|
6
|
+
passing any LangChain-style chat model — see ``refutescan.providers``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .openai_chat import openai_judge_factory, openai_navigator_factory
|
|
10
|
+
|
|
11
|
+
__all__ = ["openai_navigator_factory", "openai_judge_factory"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""OpenAI (or any OpenAI-compatible endpoint) model factories.
|
|
2
|
+
|
|
3
|
+
Requires ``langchain-openai`` (the ``openai`` extra). Set ``OPENAI_API_KEY``, or
|
|
4
|
+
pass ``base_url`` to point at a compatible gateway (vLLM, Ollama's OpenAI shim,
|
|
5
|
+
Azure OpenAI, a local proxy, …).
|
|
6
|
+
|
|
7
|
+
from refutescan import scan
|
|
8
|
+
from refutescan.adapters import openai_navigator_factory, openai_judge_factory
|
|
9
|
+
|
|
10
|
+
result = scan(
|
|
11
|
+
"https://github.com/owner/repo",
|
|
12
|
+
navigator_factory=openai_navigator_factory("gpt-4o-mini"),
|
|
13
|
+
judge_factory=openai_judge_factory("gpt-4o"),
|
|
14
|
+
)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from ..providers import JudgeFactory, NavigatorFactory
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _chat(model: str, **kwargs: Any):
|
|
25
|
+
try:
|
|
26
|
+
from langchain_openai import ChatOpenAI
|
|
27
|
+
except ImportError as e: # pragma: no cover
|
|
28
|
+
raise ImportError(
|
|
29
|
+
"refutescan's OpenAI adapter needs langchain-openai. "
|
|
30
|
+
"Install it with: pip install 'refutescan[openai]'"
|
|
31
|
+
) from e
|
|
32
|
+
return ChatOpenAI(model=model, temperature=0, **kwargs)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def openai_navigator_factory(model: str = "gpt-4o-mini", **kwargs: Any) -> NavigatorFactory:
|
|
36
|
+
"""A navigator factory using a fast OpenAI tool-calling model (the wide-net pass)."""
|
|
37
|
+
def factory():
|
|
38
|
+
return _chat(model, **kwargs)
|
|
39
|
+
return factory
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def openai_judge_factory(model: str = "gpt-4o", **kwargs: Any) -> JudgeFactory:
|
|
43
|
+
"""A judge factory using a stronger OpenAI model with structured output (refute pass)."""
|
|
44
|
+
def factory():
|
|
45
|
+
llm = _chat(model, **kwargs)
|
|
46
|
+
|
|
47
|
+
def judge(prompt: str, schema: type):
|
|
48
|
+
return llm.with_structured_output(schema).invoke(prompt)
|
|
49
|
+
|
|
50
|
+
return judge
|
|
51
|
+
return factory
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Command-line entry points.
|
|
2
|
+
|
|
3
|
+
refutescan <path-or-git-url> [options] — run a scan, print findings
|
|
4
|
+
refutescan-build-sandbox — build the docker jail image
|
|
5
|
+
|
|
6
|
+
The scan CLI uses the OpenAI adapter by default (needs the ``openai`` extra and
|
|
7
|
+
OPENAI_API_KEY, or --base-url for a compatible gateway). For other providers,
|
|
8
|
+
call ``refutescan.scan`` directly with your own factories.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from . import __version__
|
|
18
|
+
from .models import ScanConfig
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _sev_marker(sev: str) -> str:
|
|
22
|
+
return {"critical": "[CRIT]", "high": "[HIGH]", "medium": "[MED ]",
|
|
23
|
+
"low": "[LOW ]", "info": "[INFO]"}.get(sev, "[????]")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main(argv=None) -> int:
|
|
27
|
+
ap = argparse.ArgumentParser(
|
|
28
|
+
prog="refutescan",
|
|
29
|
+
description="Two-LLM, refute-first agentic source-code vulnerability scanner.")
|
|
30
|
+
ap.add_argument("source", help="a local directory path or a git URL to clone")
|
|
31
|
+
ap.add_argument("--branch", default="", help="git branch to clone (git sources only)")
|
|
32
|
+
ap.add_argument("--sandbox", default="auto", choices=["auto", "docker", "inprocess"],
|
|
33
|
+
help="isolation backend (default: auto)")
|
|
34
|
+
ap.add_argument("--navigator-model", default="gpt-4o-mini",
|
|
35
|
+
help="navigator (wide-net) model (default: gpt-4o-mini)")
|
|
36
|
+
ap.add_argument("--judge-model", default="gpt-4o",
|
|
37
|
+
help="judge (refute) model (default: gpt-4o)")
|
|
38
|
+
ap.add_argument("--base-url", default=None,
|
|
39
|
+
help="OpenAI-compatible base URL (vLLM, Azure, local proxy, …)")
|
|
40
|
+
ap.add_argument("--json", action="store_true", help="emit the full result as JSON")
|
|
41
|
+
ap.add_argument("--version", action="version", version=f"refutescan {__version__}")
|
|
42
|
+
args = ap.parse_args(argv)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
from .adapters import openai_judge_factory, openai_navigator_factory
|
|
46
|
+
except Exception as e: # pragma: no cover
|
|
47
|
+
print(f"error: {e}", file=sys.stderr)
|
|
48
|
+
return 2
|
|
49
|
+
|
|
50
|
+
kw = {"base_url": args.base_url} if args.base_url else {}
|
|
51
|
+
from . import scan
|
|
52
|
+
|
|
53
|
+
def _progress(phase: str) -> None:
|
|
54
|
+
if not args.json:
|
|
55
|
+
print(f" … {phase}", file=sys.stderr)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
result = scan(
|
|
59
|
+
args.source,
|
|
60
|
+
navigator_factory=openai_navigator_factory(args.navigator_model, **kw),
|
|
61
|
+
judge_factory=openai_judge_factory(args.judge_model, **kw),
|
|
62
|
+
branch=args.branch,
|
|
63
|
+
config=ScanConfig(sandbox=args.sandbox),
|
|
64
|
+
progress=_progress,
|
|
65
|
+
)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
print(f"scan failed: {type(e).__name__}: {e}", file=sys.stderr)
|
|
68
|
+
return 1
|
|
69
|
+
|
|
70
|
+
if args.json:
|
|
71
|
+
print(result.model_dump_json(indent=2))
|
|
72
|
+
return 0 if not result.findings else 1
|
|
73
|
+
|
|
74
|
+
s = result.summary
|
|
75
|
+
print(f"\nScanned {s.get('files_scanned')} files / {s.get('total_loc')} LOC"
|
|
76
|
+
f" · {'sandboxed' if result.sandboxed else 'in-process'}")
|
|
77
|
+
print(f"{result.candidate_count} candidate(s) → {len(result.findings)} confirmed, "
|
|
78
|
+
f"{len(result.culled)} culled\n")
|
|
79
|
+
if not result.findings:
|
|
80
|
+
print("No confirmed findings.")
|
|
81
|
+
return 0
|
|
82
|
+
for f in result.findings:
|
|
83
|
+
conf = f.get("confidence")
|
|
84
|
+
conf_s = f" (conf {conf:.2f})" if isinstance(conf, (int, float)) else ""
|
|
85
|
+
print(f"{_sev_marker(f.get('severity', ''))} {f.get('title')}{conf_s}")
|
|
86
|
+
print(f" {f.get('file')}:{f.get('line')} [{f.get('vuln_class')}]")
|
|
87
|
+
if f.get("reasoning"):
|
|
88
|
+
print(f" {f['reasoning']}")
|
|
89
|
+
print()
|
|
90
|
+
return 1 # non-zero exit when findings exist (CI-friendly)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def build_sandbox(argv=None) -> int:
|
|
94
|
+
"""Build the docker jail image (refutescan-build-sandbox)."""
|
|
95
|
+
from .sandbox.docker import sandbox_dir
|
|
96
|
+
ap = argparse.ArgumentParser(
|
|
97
|
+
prog="refutescan-build-sandbox",
|
|
98
|
+
description="Build the refutescan docker sandbox image.")
|
|
99
|
+
ap.add_argument("--image", default="refutescan-sandbox:current", help="image tag to build")
|
|
100
|
+
args = ap.parse_args(argv)
|
|
101
|
+
d = sandbox_dir()
|
|
102
|
+
print(f"Building {args.image} from {d} ...")
|
|
103
|
+
try:
|
|
104
|
+
subprocess.run(["docker", "build", "-t", args.image, str(d)], check=True)
|
|
105
|
+
except FileNotFoundError:
|
|
106
|
+
print("error: docker not found on PATH", file=sys.stderr)
|
|
107
|
+
return 2
|
|
108
|
+
except subprocess.CalledProcessError as e:
|
|
109
|
+
print(f"docker build failed ({e.returncode})", file=sys.stderr)
|
|
110
|
+
return e.returncode
|
|
111
|
+
print(f"Done. Image: {args.image}")
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
sys.exit(main())
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Deterministic, LLM-free repository map.
|
|
2
|
+
|
|
3
|
+
A single os.walk that classifies files (source / manifest / entrypoint), counts
|
|
4
|
+
LOC, and returns the relative source-file list the navigator will reach. Cheap,
|
|
5
|
+
reproducible, and the same logic that runs inside the sandbox toolrunner.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Tuple
|
|
13
|
+
|
|
14
|
+
# Source file extensions worth scanning. Anything else (assets, lockfiles,
|
|
15
|
+
# binaries, minified bundles) is counted as context, not handed to the LLM.
|
|
16
|
+
SOURCE_EXTS = {
|
|
17
|
+
".py", ".js", ".jsx", ".ts", ".tsx", ".java", ".go", ".rb", ".php", ".cs",
|
|
18
|
+
".c", ".cc", ".cpp", ".h", ".hpp", ".rs", ".kt", ".scala", ".swift", ".sh",
|
|
19
|
+
".bash", ".pl", ".pm", ".sql", ".html", ".vue", ".lua", ".groovy", ".tf",
|
|
20
|
+
}
|
|
21
|
+
# Directories never worth walking — vendored / generated / VCS metadata.
|
|
22
|
+
SKIP_DIRS = {
|
|
23
|
+
".git", ".hg", ".svn", "node_modules", "vendor", "venv", ".venv", "env",
|
|
24
|
+
"__pycache__", ".mypy_cache", ".pytest_cache", "dist", "build", ".next",
|
|
25
|
+
"site-packages", "target", ".idea", ".vscode", ".gradle", "bin", "obj",
|
|
26
|
+
"coverage", ".tox", ".cache", "bower_components",
|
|
27
|
+
}
|
|
28
|
+
# Dependency manifests we surface in the map (signal for what the app is).
|
|
29
|
+
MANIFESTS = {
|
|
30
|
+
"requirements.txt", "pyproject.toml", "setup.py", "Pipfile", "package.json",
|
|
31
|
+
"go.mod", "pom.xml", "build.gradle", "Gemfile", "composer.json", "Cargo.toml",
|
|
32
|
+
"csproj",
|
|
33
|
+
}
|
|
34
|
+
# Entry-point filename hints (heuristic — where untrusted input often lands).
|
|
35
|
+
ENTRY_HINTS = {
|
|
36
|
+
"app.py", "main.py", "manage.py", "wsgi.py", "asgi.py", "server.py",
|
|
37
|
+
"index.js", "server.js", "app.js", "main.go", "main.rs", "index.php",
|
|
38
|
+
"application.java",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def build_map(root: Path, max_map_files: int = 400,
|
|
43
|
+
max_file_bytes: int = 200_000) -> Tuple[Dict[str, Any], List[str]]:
|
|
44
|
+
"""Walk the tree once. Returns (map_dict, relative_source_file_paths)."""
|
|
45
|
+
root = root.resolve()
|
|
46
|
+
langs: Dict[str, int] = {}
|
|
47
|
+
manifests: List[str] = []
|
|
48
|
+
entrypoints: List[str] = []
|
|
49
|
+
source_files: List[str] = []
|
|
50
|
+
total_files = 0
|
|
51
|
+
total_loc = 0
|
|
52
|
+
|
|
53
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
54
|
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith(".")]
|
|
55
|
+
for fn in filenames:
|
|
56
|
+
total_files += 1
|
|
57
|
+
ext = os.path.splitext(fn)[1].lower()
|
|
58
|
+
full = Path(dirpath) / fn
|
|
59
|
+
rel = str(full.relative_to(root))
|
|
60
|
+
if fn in MANIFESTS or fn.endswith(".csproj"):
|
|
61
|
+
manifests.append(rel)
|
|
62
|
+
if fn in ENTRY_HINTS:
|
|
63
|
+
entrypoints.append(rel)
|
|
64
|
+
if ext in SOURCE_EXTS and len(source_files) < max_map_files:
|
|
65
|
+
source_files.append(rel)
|
|
66
|
+
langs[ext] = langs.get(ext, 0) + 1
|
|
67
|
+
try:
|
|
68
|
+
if full.stat().st_size <= max_file_bytes:
|
|
69
|
+
with open(full, "r", errors="ignore") as fh:
|
|
70
|
+
total_loc += sum(1 for _ in fh)
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
code_map = {
|
|
75
|
+
"root_name": root.name,
|
|
76
|
+
"total_files": total_files,
|
|
77
|
+
"source_files_scanned": len(source_files),
|
|
78
|
+
"truncated": len(source_files) >= max_map_files,
|
|
79
|
+
"total_loc": total_loc,
|
|
80
|
+
"languages": dict(sorted(langs.items(), key=lambda kv: -kv[1])),
|
|
81
|
+
"manifests": manifests[:30],
|
|
82
|
+
"entrypoints": entrypoints[:30],
|
|
83
|
+
}
|
|
84
|
+
return code_map, source_files
|