scanfuse 0.2.0__py3-none-any.whl
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.
- scanfuse/__init__.py +4 -0
- scanfuse/cli.py +235 -0
- scanfuse/engine.py +503 -0
- scanfuse/html_report.py +23 -0
- scanfuse/models.py +54 -0
- scanfuse/parsers/__init__.py +3 -0
- scanfuse/parsers/base.py +142 -0
- scanfuse/parsers/burp.py +135 -0
- scanfuse/parsers/nessus.py +126 -0
- scanfuse/parsers/nmap.py +137 -0
- scanfuse/parsers/nuclei.py +157 -0
- scanfuse/parsers/trivy.py +187 -0
- scanfuse/parsers/zap.py +124 -0
- scanfuse/templates/finding.md.j2 +67 -0
- scanfuse/templates/index.md.j2 +44 -0
- scanfuse/utils.py +184 -0
- scanfuse-0.2.0.dist-info/METADATA +208 -0
- scanfuse-0.2.0.dist-info/RECORD +21 -0
- scanfuse-0.2.0.dist-info/WHEEL +4 -0
- scanfuse-0.2.0.dist-info/entry_points.txt +2 -0
- scanfuse-0.2.0.dist-info/licenses/LICENSE +21 -0
scanfuse/utils.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from scanfuse.models import Evidence, Finding, Host, SourceRecord
|
|
4
|
+
|
|
5
|
+
# Names that must not be used as a file name on Windows, plus the files scanfuse writes itself.
|
|
6
|
+
RESERVED_SLUGS = frozenset(
|
|
7
|
+
{"index", "report", "con", "prn", "aux", "nul"}
|
|
8
|
+
| {f"com{i}" for i in range(1, 10)}
|
|
9
|
+
| {f"lpt{i}" for i in range(1, 10)}
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
SEVERITY_ORDER = {"Info": 0, "Low": 1, "Medium": 2, "High": 3, "Critical": 4}
|
|
13
|
+
|
|
14
|
+
# Identifiers that name the same weakness in every tool. Only these (together with title and
|
|
15
|
+
# port) decide whether two findings are the same. Tool ids like 'nessus-plugin:123' do not.
|
|
16
|
+
_GLOBAL_ID = re.compile(r"^(CVE-\d{4}-\d{4,}|GHSA(-[0-9a-z]{4}){3})$", re.IGNORECASE)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def slugify(text: str) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Convert text to a safe, lower-cased filename slug.
|
|
22
|
+
Example: 'SQL Injection (Time-based)' -> 'sql-injection-time-based'
|
|
23
|
+
"""
|
|
24
|
+
text = text.lower()
|
|
25
|
+
text = re.sub(r'[^a-z0-9]+', '-', text)
|
|
26
|
+
result = text[:100].strip('-')
|
|
27
|
+
return result if result else "unknown-finding"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def unique_slug(title: str, seen: set[str]) -> str:
|
|
31
|
+
"""Return a slug that is not in `seen` and is not a reserved file name. Adds it to `seen`."""
|
|
32
|
+
base = slugify(title)
|
|
33
|
+
slug = base
|
|
34
|
+
counter = 1
|
|
35
|
+
while slug in seen or slug in RESERVED_SLUGS:
|
|
36
|
+
slug = f"{base}-{counter}"
|
|
37
|
+
counter += 1
|
|
38
|
+
seen.add(slug)
|
|
39
|
+
return slug
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _global_ids(f: Finding) -> frozenset[str]:
|
|
43
|
+
return frozenset(i.upper() for i in f.identifiers if _GLOBAL_ID.match(i))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _local_ids(f: Finding) -> dict[str, frozenset[str]]:
|
|
47
|
+
"""Tool specific ids grouped by prefix, e.g. {'nessus-plugin': {'12345'}}."""
|
|
48
|
+
grouped: dict[str, set[str]] = {}
|
|
49
|
+
for ident in f.identifiers:
|
|
50
|
+
if ":" in ident:
|
|
51
|
+
prefix, _, value = ident.partition(":")
|
|
52
|
+
grouped.setdefault(prefix, set()).add(value)
|
|
53
|
+
return {k: frozenset(v) for k, v in grouped.items()}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _identity(f: Finding) -> tuple[str, frozenset[str], str | None]:
|
|
57
|
+
return (f.title.strip().casefold(), _global_ids(f), f.port)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _compatible(group_ids: dict[str, frozenset[str]], f: Finding) -> bool:
|
|
61
|
+
"""Two findings of the same tool with different tool ids are different findings."""
|
|
62
|
+
for prefix, values in _local_ids(f).items():
|
|
63
|
+
if prefix in group_ids and group_ids[prefix] != values:
|
|
64
|
+
return False
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _record(f: Finding) -> SourceRecord:
|
|
69
|
+
return SourceRecord(
|
|
70
|
+
source_tool=f.source_tool, title=f.title, severity=f.severity,
|
|
71
|
+
cvss_score=f.cvss_score, cvss_vector=f.cvss_vector, description=f.description,
|
|
72
|
+
remediation=f.remediation, identifiers=list(f.identifiers), port=f.port,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _merge_os(existing: str | None, new: str | None) -> str | None:
|
|
77
|
+
if not existing:
|
|
78
|
+
return new
|
|
79
|
+
if not new or new == existing or new in existing.split(" / "):
|
|
80
|
+
return existing
|
|
81
|
+
return f"{existing} / {new}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def deduplicate_findings(findings: list[Finding]) -> list[Finding]:
|
|
85
|
+
"""
|
|
86
|
+
Merge findings that are the same finding reported more than once.
|
|
87
|
+
|
|
88
|
+
Two findings are the same if their titles are equal (ignoring case and surrounding space),
|
|
89
|
+
they carry the same CVE/GHSA ids, they have the same port, and they do not carry different
|
|
90
|
+
ids of the same tool. Similar but not equal titles are never merged.
|
|
91
|
+
|
|
92
|
+
A merged finding keeps every host and every piece of evidence. Evidence is tagged with the
|
|
93
|
+
tool that reported it. The description, remediation, severity and CVSS data of every
|
|
94
|
+
original finding stay available in `sources`. The input objects are not modified. The order
|
|
95
|
+
follows the first occurrence of each finding.
|
|
96
|
+
"""
|
|
97
|
+
# Each group: [merged finding, tool ids, originals]
|
|
98
|
+
groups: list[list] = []
|
|
99
|
+
by_identity: dict[tuple[str, frozenset[str], str | None], list[int]] = {}
|
|
100
|
+
|
|
101
|
+
for f in findings:
|
|
102
|
+
key = _identity(f)
|
|
103
|
+
target = None
|
|
104
|
+
for idx in by_identity.get(key, ()):
|
|
105
|
+
if _compatible(groups[idx][1], f):
|
|
106
|
+
target = groups[idx]
|
|
107
|
+
break
|
|
108
|
+
if target is None:
|
|
109
|
+
by_identity.setdefault(key, []).append(len(groups))
|
|
110
|
+
groups.append([f, dict(_local_ids(f)), [f]])
|
|
111
|
+
else:
|
|
112
|
+
target[2].append(f)
|
|
113
|
+
for prefix, values in _local_ids(f).items():
|
|
114
|
+
target[1].setdefault(prefix, values)
|
|
115
|
+
|
|
116
|
+
result: list[Finding] = []
|
|
117
|
+
for first, _ids, originals in groups:
|
|
118
|
+
if len(originals) == 1:
|
|
119
|
+
result.append(first)
|
|
120
|
+
else:
|
|
121
|
+
result.append(_merge_group(originals))
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _merge_group(originals: list[Finding]) -> Finding:
|
|
126
|
+
base = originals[0].model_copy(deep=True)
|
|
127
|
+
base.sources = [_record(f) for f in originals]
|
|
128
|
+
|
|
129
|
+
tools: list[str] = []
|
|
130
|
+
for f in originals:
|
|
131
|
+
for tool in f.source_tool.split(","):
|
|
132
|
+
tool = tool.strip()
|
|
133
|
+
if tool and tool not in tools:
|
|
134
|
+
tools.append(tool)
|
|
135
|
+
base.source_tool = ", ".join(tools)
|
|
136
|
+
|
|
137
|
+
host_index: dict[tuple[str, str | None], Host] = {}
|
|
138
|
+
base.hosts = []
|
|
139
|
+
evidence_index: dict[tuple[str, str], Evidence] = {}
|
|
140
|
+
base.evidence = []
|
|
141
|
+
identifiers: list[str] = []
|
|
142
|
+
|
|
143
|
+
for f in originals:
|
|
144
|
+
for host in f.hosts:
|
|
145
|
+
key = (host.ip, host.hostname)
|
|
146
|
+
known = host_index.get(key)
|
|
147
|
+
if known is None:
|
|
148
|
+
copy = host.model_copy()
|
|
149
|
+
host_index[key] = copy
|
|
150
|
+
base.hosts.append(copy)
|
|
151
|
+
else:
|
|
152
|
+
known.os = _merge_os(known.os, host.os)
|
|
153
|
+
for ev in f.evidence:
|
|
154
|
+
known_ev = evidence_index.get((ev.type, ev.data))
|
|
155
|
+
tool = ev.source or f.source_tool
|
|
156
|
+
if known_ev is None:
|
|
157
|
+
tagged = ev.model_copy(update={"source": tool})
|
|
158
|
+
evidence_index[(ev.type, ev.data)] = tagged
|
|
159
|
+
base.evidence.append(tagged)
|
|
160
|
+
else:
|
|
161
|
+
# Same evidence from another tool: keep one copy and list every tool.
|
|
162
|
+
tools_seen = (known_ev.source or "").split(", ")
|
|
163
|
+
if tool not in tools_seen:
|
|
164
|
+
known_ev.source = ", ".join([t for t in tools_seen if t] + [tool])
|
|
165
|
+
for ident in f.identifiers:
|
|
166
|
+
if ident not in identifiers:
|
|
167
|
+
identifiers.append(ident)
|
|
168
|
+
base.identifiers = identifiers
|
|
169
|
+
base.references = list(dict.fromkeys(url for f in originals for url in f.references))
|
|
170
|
+
|
|
171
|
+
# Summary values: highest severity, highest CVSS score with its own vector.
|
|
172
|
+
base.severity = max((f.severity for f in originals), key=lambda s: SEVERITY_ORDER.get(s, 0))
|
|
173
|
+
scored = [f for f in originals if f.cvss_score is not None]
|
|
174
|
+
if scored:
|
|
175
|
+
best = max(scored, key=lambda f: f.cvss_score or 0.0)
|
|
176
|
+
base.cvss_score = best.cvss_score
|
|
177
|
+
base.cvss_vector = best.cvss_vector
|
|
178
|
+
else:
|
|
179
|
+
base.cvss_score = None
|
|
180
|
+
base.cvss_vector = next((f.cvss_vector for f in originals if f.cvss_vector), None)
|
|
181
|
+
# Text: first non-empty value; every original text stays in `sources`.
|
|
182
|
+
base.description = next((f.description for f in originals if f.description), base.description)
|
|
183
|
+
base.remediation = next((f.remediation for f in originals if f.remediation), None)
|
|
184
|
+
return base
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: scanfuse
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Fast, local-first pentest finding extractor & markdown report engine.
|
|
5
|
+
Project-URL: Repository, https://github.com/gencay-e/scanfuse
|
|
6
|
+
Project-URL: Changelog, https://github.com/gencay-e/scanfuse/blob/main/CHANGELOG.md
|
|
7
|
+
Author: Gencay
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: burp,nessus,nmap,nuclei,pentest,report,security,trivy,zap
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Information Technology
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Requires-Dist: defusedxml<1,>=0.7.1
|
|
21
|
+
Requires-Dist: jinja2<4,>=3.1.3
|
|
22
|
+
Requires-Dist: markdown<4,>=3.11
|
|
23
|
+
Requires-Dist: nh3<1,>=0.3.7
|
|
24
|
+
Requires-Dist: pydantic<3,>=2.7.0
|
|
25
|
+
Requires-Dist: pyyaml<7,>=6.0.1
|
|
26
|
+
Requires-Dist: rich<16,>=13.7.0
|
|
27
|
+
Requires-Dist: typer<1,>=0.12.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: hypothesis>=6.100.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: mypy>=1.9.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pyinstaller>=6.5.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.3.0; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# scanfuse
|
|
37
|
+
|
|
38
|
+
A local command-line tool that reads pentest scanner reports and writes one report from them.
|
|
39
|
+
|
|
40
|
+
<img alt="Scan files from Nmap, Nuclei, ZAP, Burp, Nessus and Trivy go into scanfuse, which writes index.md, report.html, report.json and report.csv" src="docs/images/scanfuse-flow.svg" width="900">
|
|
41
|
+
|
|
42
|
+
It imports Nmap, Nuclei, ZAP, Burp, Nessus and Trivy files and writes Markdown (with an index and a standalone HTML page), JSON or CSV. It runs offline and sends nothing anywhere.
|
|
43
|
+
|
|
44
|
+
Version 0.2.0, a pre-release. See [CHANGELOG.md](CHANGELOG.md).
|
|
45
|
+
|
|
46
|
+
## Example
|
|
47
|
+
|
|
48
|
+
<img alt="The summary of a report" src="docs/images/report-summary.png" width="900">
|
|
49
|
+
|
|
50
|
+
<img alt="A finding in the report" src="docs/images/report-finding.png" width="900">
|
|
51
|
+
|
|
52
|
+
`report.html` follows the light or dark setting of your system; the screenshots show the dark one.
|
|
53
|
+
|
|
54
|
+
The report above was made from six real scan files (Nmap, ZAP, Trivy, Burp, Nessus). The input files, the
|
|
55
|
+
command and the full report are in [examples/](examples/).
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
You need Python 3.12 or newer.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pipx install scanfuse # from PyPI
|
|
63
|
+
pipx install git+https://github.com/gencay-e/scanfuse # latest state from GitHub
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`pip install scanfuse` works as well. From a checkout of this repository: `pipx install .`
|
|
67
|
+
|
|
68
|
+
Ready-made programs for Linux, Windows and macOS (no Python needed) are on the [Releases page](https://github.com/gencay-e/scanfuse/releases), with a `SHA256SUMS` file. To build one yourself, see [Build a standalone program](#build-a-standalone-program).
|
|
69
|
+
|
|
70
|
+
## Use
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
scanfuse scan.xml -o report
|
|
74
|
+
scanfuse nmap.xml nuclei.json zap.xml -o report
|
|
75
|
+
scanfuse scan.json -f trivy --format-out json -o report
|
|
76
|
+
scanfuse scan.xml --min-severity High --host 192.0.2.1 -o report
|
|
77
|
+
cat scan.json | scanfuse -o report
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`scanfuse` takes one or more files, or `-` for standard input. It finds the format by itself. Use `-f` to choose one: `nmap`, `nuclei`, `zap`, `burp`, `nessus`, `trivy`.
|
|
81
|
+
|
|
82
|
+
| Option | Meaning |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `-o`, `--output` | Output directory. Default `report`. |
|
|
85
|
+
| `--format-out` | `markdown` (default), `json` or `csv`. `markdown` writes one file per finding, `index.md` and `report.html`. |
|
|
86
|
+
| `-s`, `--min-severity` | `Critical`, `High`, `Medium`, `Low` or `Info`. Case does not matter. |
|
|
87
|
+
| `--host` | Only findings on this host. Must match the IP or host name exactly, ignoring case. `*` and `?` are wildcards, for example `10.0.0.*`. |
|
|
88
|
+
| `-t`, `--template-dir` | Folder with your own `finding.md.j2` and `index.md.j2`. The folder must exist. Missing files fall back to the built-in templates. |
|
|
89
|
+
| `--replace` | Overwrite an existing report. See below. |
|
|
90
|
+
| `--csv-excel` | CSV with a byte order mark and `;` as separator, which German-language Excel expects. |
|
|
91
|
+
| `--version` | Print the version. |
|
|
92
|
+
|
|
93
|
+
### Existing reports
|
|
94
|
+
|
|
95
|
+
`scanfuse` never overwrites files by accident. If the output directory already contains a file that the new report would write, it stops with exit code 2 and changes nothing. Add `--replace` to overwrite the report. With `--replace`, files of the earlier report that are no longer part of it are removed. Files that scanfuse did not write are left alone.
|
|
96
|
+
|
|
97
|
+
The report is built in a temporary folder first. If moving the files fails, scanfuse tries to restore the old report. If restoration also fails, the error gives the location of the retained backup. A process or system crash during replacement can still leave a mixed report.
|
|
98
|
+
|
|
99
|
+
### Incomplete runs
|
|
100
|
+
|
|
101
|
+
If one input file cannot be read, scanfuse still writes the findings from the other files and exits with code 1. The report says so:
|
|
102
|
+
|
|
103
|
+
* `report.json`: `"complete": false` and the list of `errors`.
|
|
104
|
+
* `index.md` and `report.html`: an **INCOMPLETE** notice with the errors.
|
|
105
|
+
* `report.csv`: the column `Import Status` says `incomplete (...)` in every row.
|
|
106
|
+
|
|
107
|
+
An invalid `.scanfuse.yaml` counts as an error too. The report is then incomplete, because ignore rules were not applied.
|
|
108
|
+
|
|
109
|
+
### Exit codes
|
|
110
|
+
|
|
111
|
+
| Code | Meaning |
|
|
112
|
+
|---|---|
|
|
113
|
+
| 0 | Complete. |
|
|
114
|
+
| 1 | Incomplete or failed: an input could not be read, `.scanfuse.yaml` is invalid, nothing could be written, or no input was given. |
|
|
115
|
+
| 2 | Invalid option, or the output files already exist (use `--replace`). |
|
|
116
|
+
|
|
117
|
+
## Merging duplicates
|
|
118
|
+
|
|
119
|
+
Findings from several files are merged when they are the same finding:
|
|
120
|
+
|
|
121
|
+
* the titles are equal, ignoring case and surrounding spaces,
|
|
122
|
+
* they carry the same CVE or GHSA ids,
|
|
123
|
+
* they have the same port, and
|
|
124
|
+
* the same tool does not give them different tool ids (for example two Nessus plugin ids).
|
|
125
|
+
|
|
126
|
+
Titles that are only similar are not merged. A merged finding keeps all hosts and all evidence. Evidence shows which tool reported it. What each tool wrote (description, remediation, severity, CVSS score and vector, ids) is listed under **Reported by** in the Markdown and in `sources` in the JSON. The summary severity is the highest of all reports. The summary CVSS score is the highest score, together with its own vector.
|
|
127
|
+
|
|
128
|
+
## Output
|
|
129
|
+
|
|
130
|
+
### JSON
|
|
131
|
+
|
|
132
|
+
`report.json` is an object:
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"schema_version": 2,
|
|
137
|
+
"complete": true,
|
|
138
|
+
"errors": [],
|
|
139
|
+
"findings": [ { "title": "...", "severity": "High", "identifiers": ["CVE-2024-0001"], "port": "443/tcp", "sources": [] } ]
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### CSV
|
|
144
|
+
|
|
145
|
+
Columns: `Title`, `Severity`, `CVSS`, `Hosts`, `Source Tool`, `Description`, `Remediation`, `Evidence`, `CVSS Vector`, `Port`, `Identifiers`, `References`, `Other Reports`, `Import Status`. `Hosts` shows `ip (host name)` when a host name is known.
|
|
146
|
+
|
|
147
|
+
Cells that start with `=`, `+`, `-` or `@` get a leading `'` to reduce the risk of spreadsheet formulas. This also covers look-alike characters such as a full-width `=` and a leading no-break space. These cases were tested by reading the files back.
|
|
148
|
+
|
|
149
|
+
In Excel, import `--csv-excel` output with **Data > From Text/CSV** and use the semicolon delimiter. In a Windows Excel test, this preserved a multiline evidence field; the older Text Import Wizard split it across rows. Formula handling and LibreOffice remain unverified in those applications.
|
|
150
|
+
|
|
151
|
+
### Scanner text in the report
|
|
152
|
+
|
|
153
|
+
Scanner output is untrusted. scanfuse treats it like this:
|
|
154
|
+
|
|
155
|
+
* Titles, host names, ids and tool names are shown as plain text. They cannot add links, headings or HTML.
|
|
156
|
+
* Descriptions and remediation text keep Markdown formatting, including links. Raw HTML is escaped, and the text cannot create a heading. A link in a scanner description is shown as the scanner wrote it, so check it before you click.
|
|
157
|
+
* Evidence is shown unchanged in a code block.
|
|
158
|
+
* The HTML page removes scripts, images, frames and `javascript:` links.
|
|
159
|
+
* XML with entity declarations is rejected.
|
|
160
|
+
|
|
161
|
+
## Configuration: `.scanfuse.yaml`
|
|
162
|
+
|
|
163
|
+
`scanfuse` reads `.scanfuse.yaml` from the current directory.
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
ignore_titles:
|
|
167
|
+
- "Wappalyzer"
|
|
168
|
+
- "Header Missing"
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Findings whose title contains one of these texts (ignoring case) are left out. `ignore_titles` must be a list of strings. A single string is read as a list with one entry. Anything else, a file that is not a mapping, or a YAML syntax error is reported as an error and the rules are not applied.
|
|
172
|
+
|
|
173
|
+
## Build a standalone program
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
uv sync --extra dev
|
|
177
|
+
uv run python build.py
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The program is written to `dist/`. To check it against the test suite:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
SCANFUSE_EXE=dist/scanfuse uv run pytest tests/test_packaged_cli.py # Linux, macOS
|
|
184
|
+
$env:SCANFUSE_EXE = (Resolve-Path .\dist\scanfuse.exe).Path # Windows PowerShell
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
A program built on Linux depends on the C library of the system it was built on. Build on the oldest system you want to support.
|
|
188
|
+
|
|
189
|
+
## Development
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
uv sync --locked --extra dev
|
|
193
|
+
uv run ruff check .
|
|
194
|
+
uv run mypy scanfuse
|
|
195
|
+
uv run pytest tests/
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Limits
|
|
199
|
+
|
|
200
|
+
* scanfuse was tested with real output from nmap (7.94, 7.12, 6.40), ZAP 1.4, Burp, Nessus and Trivy (three file layouts), taken from the public [DefectDojo sample-scan-files](https://github.com/DefectDojo/sample-scan-files). Those files are old. Newer versions of the tools may add fields that scanfuse does not read yet. Nuclei has no real sample file in the tests. Open an issue with a cleaned sample if a file fails.
|
|
201
|
+
* Format detection looks at the start of the file. Use `-f` if it guesses wrong.
|
|
202
|
+
* The HTML page and the CSV file were checked by reading the files. They were not opened by hand in a browser, Excel or LibreOffice.
|
|
203
|
+
* 100,000 results were processed completely in all three output formats on one Linux machine (4 CPUs, 16 GB). JSON took 14 seconds, CSV 10 seconds, Markdown with HTML 124 seconds, and the peak memory was between 0.7 and 1.0 GB. The HTML page for that many findings is large. You can repeat the check with `python tools/large_check.py 100000`.
|
|
204
|
+
* Standalone programs are built and tested in CI on Ubuntu 22.04 (x86_64), Windows (x86_64) and macOS (arm64). Other Linux distributions, Linux on ARM and macOS on Intel were not tested.
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
scanfuse/__init__.py,sha256=dJrjg3JsK3QAihGLinGdgf_A4LpgeYy0u74MdkNnj5s,84
|
|
2
|
+
scanfuse/cli.py,sha256=riaKbAExNhAHwEW_wJMq2upl_Fl2U6DTjHq93TjpREg,9153
|
|
3
|
+
scanfuse/engine.py,sha256=fYkn8ZaGhZrTuMg-V2x97vDFVWCkCK9ezS-6dqYAnOI,23445
|
|
4
|
+
scanfuse/html_report.py,sha256=fQNMk7_oiCYUktjjj5gRTDPkQZpiCr8NPZmDMM-HlJA,694
|
|
5
|
+
scanfuse/models.py,sha256=aDLdP-H7riqZx0rJBPlvLzVxIBxbqUShQCMyIzIQ6NE,1993
|
|
6
|
+
scanfuse/utils.py,sha256=qBDUbto7kqEm1vsVhsjTvS_tAsb6N7ltU3BKo-or1-I,7209
|
|
7
|
+
scanfuse/parsers/__init__.py,sha256=L3_KOxgzaJQPN9a4HRkgIimGVpL_s8LLuy7zQ-ZbuKI,53
|
|
8
|
+
scanfuse/parsers/base.py,sha256=meJhEc-4VzyFVvHGWqeia2rEza7_ojb5_aBdbK_Fnmo,4654
|
|
9
|
+
scanfuse/parsers/burp.py,sha256=E7Pt33uoNDjsc23lvgO1xxqrjmnZKAgU3Clu9r72Aps,6124
|
|
10
|
+
scanfuse/parsers/nessus.py,sha256=H8V4Htplzq4J9eBG4z5yd3pGgCKl3KJSxUThk0DKN_Q,5733
|
|
11
|
+
scanfuse/parsers/nmap.py,sha256=trEXWUkvTxYpG_8KqT45tLYVi5MvomdZlS95hdO-GXY,5830
|
|
12
|
+
scanfuse/parsers/nuclei.py,sha256=weukJn1Bl605MjLjnhPwv91l-ToCNXnL42weYqBFBxE,5425
|
|
13
|
+
scanfuse/parsers/trivy.py,sha256=0-In3j1sd6e-pC2Hvn8TRisDTU1NauLlt4x9ZbMxHBA,8370
|
|
14
|
+
scanfuse/parsers/zap.py,sha256=nqkHeL1UlHdhB2brSxQYBOHkkQoH9MnG2qSQskJ8pn8,5333
|
|
15
|
+
scanfuse/templates/finding.md.j2,sha256=Ki8T5Nmq24mT6ekUXRyNpVU5XJVlr4OTN_70JJYkLpU,1647
|
|
16
|
+
scanfuse/templates/index.md.j2,sha256=tCjvKRhYuv9oSowEbr5pf_Z3BkJDr00R8vjD1WE0DoQ,1446
|
|
17
|
+
scanfuse-0.2.0.dist-info/METADATA,sha256=3LWLGFcgYtR0hMx-cJz54ggKNVDuICe8S_SyQnp5Rzc,10266
|
|
18
|
+
scanfuse-0.2.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
19
|
+
scanfuse-0.2.0.dist-info/entry_points.txt,sha256=31QZsLdhrPN7xa0cezdGDmxtXTvUK6C1xws66xp1OSA,46
|
|
20
|
+
scanfuse-0.2.0.dist-info/licenses/LICENSE,sha256=yXy4VpeLDAfRK0Bucm6wn3hjAAUmMcEjhbNjfH6ujbI,1063
|
|
21
|
+
scanfuse-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gencay
|
|
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.
|