Wuhp commited on
Commit ·
72f5723
1
Parent(s): 14929e4
Use native TruffleHog verification across all built-in detectors
Browse files- README.md +7 -5
- app/final_modules.py +47 -91
- app/index.html +2 -2
- tests/test_final_modules.py +85 -97
- tests/trufflehog_offline_smoke.py +1 -3
README.md
CHANGED
|
@@ -7,15 +7,17 @@ app_port: 7860
|
|
| 7 |
|
| 8 |
## Optional TruffleHog verification
|
| 9 |
|
| 10 |
-
In **Settings → Module inputs**, supply source text and select **Verify found credentials I own or am authorized to test**. Select TruffleHog in the pipeline as usual. Verification is off by default, is not enabled by Full pipeline, and resets after a successful scan submission. No separate provider API key is required:
|
| 11 |
|
| 12 |
-
|
| 13 |
|
| 14 |
-
|
| 15 |
|
| 16 |
-
|
| 17 |
|
| 18 |
-
|
|
|
|
|
|
|
| 19 |
|
| 20 |
## Direct URLs and target-list imports
|
| 21 |
|
|
|
|
| 7 |
|
| 8 |
## Optional TruffleHog verification
|
| 9 |
|
| 10 |
+
In **Settings → Module inputs**, supply source text and select **Verify found credentials I own or am authorized to test**. Select TruffleHog in the pipeline as usual. Verification is off by default, is not enabled by Full pipeline, and resets after a successful scan submission. No separate provider API key is required: native detectors use detected credentials to check the relevant service from the HF worker.
|
| 11 |
|
| 12 |
+
The adapter runs one native `trufflehog filesystem` scan against the **original source**, with verification enabled by omitting `--no-verification`. All built-in detector types are available: there is no application provider allowlist, per-credential source reconstruction, extra verification pass or 10-candidate cap. TruffleHog handles credential pairing, endpoint/context selection, verification support, caching, overlap decisions and provider requests. This can contact a database or host identified in source, rather than only one of five public providers. The app does not request separate resource/permission analysis or supply custom verifier URLs.
|
| 13 |
|
| 14 |
+
For HF, the process uses one worker and a 120-second overall deadline (40 seconds when verification is off). Deadlines retain parsed partial output and remain timeout results, never clean scans. No source archive extraction or binary scanning is enabled. Native detection/verification does not imply every credential can be verified successfully: detector support, service reachability and provider restrictions still apply.
|
| 15 |
|
| 16 |
+
Findings preserve native **Verified**, **Unverified** and **Inconclusive** (verification error) outcomes. Disabled or explicitly skipped verification is **Detection-only**. The pinned native JSON does not always distinguish a skipped/unsupported verifier from an unverified credential, so unverified must never be interpreted as invalid or as proof a request was made. Error results mark an otherwise completed stage partial. Logs report native mode, deadline and result counts; they do not invent a count of network verification attempts.
|
| 17 |
|
| 18 |
+
Secret values, source snippets, identity metadata and provider error text are never returned in results or logs. Source files are temporary on HF and removed after use. API clients opt in with `options.inputs.trufflehog_verify: true` alongside `source_text`. Only the setting, source byte count and sanitized result counts are persisted.
|
| 19 |
+
|
| 20 |
+
This uses the [pinned TruffleHog verification behavior](https://github.com/trufflesecurity/trufflehog/blob/v3.99.7/README.md). GitHub-hosted tests mock native verification output; the installed-binary smoke remains explicitly `--no-verification` and does not authenticate real credentials.
|
| 21 |
|
| 22 |
## Direct URLs and target-list imports
|
| 23 |
|
app/final_modules.py
CHANGED
|
@@ -4,7 +4,6 @@ import base64
|
|
| 4 |
import os
|
| 5 |
import re
|
| 6 |
import tempfile
|
| 7 |
-
import time
|
| 8 |
from pathlib import Path
|
| 9 |
from urllib.parse import urljoin, urlsplit
|
| 10 |
|
|
@@ -44,15 +43,27 @@ def json_rows(rows):
|
|
| 44 |
return found
|
| 45 |
|
| 46 |
|
| 47 |
-
#
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
|
| 53 |
def trufflehog_candidates(rows):
|
| 54 |
# Keys and native metadata remain in memory only, never in returned diagnostics.
|
| 55 |
candidates = {}
|
|
|
|
| 56 |
for row in json_rows(rows):
|
| 57 |
name = row.get("DetectorName")
|
| 58 |
if not isinstance(name, str) or not re.fullmatch(r"[A-Za-z][A-Za-z0-9_]{0,79}", name):
|
|
@@ -60,80 +71,29 @@ def trufflehog_candidates(rows):
|
|
| 60 |
raw = row.get("RawV2") or row.get("Raw") or ""
|
| 61 |
if not isinstance(raw, str):
|
| 62 |
continue
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
| 64 |
return candidates
|
| 65 |
|
| 66 |
|
| 67 |
-
def
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
return None
|
| 71 |
-
if name == "AWS":
|
| 72 |
-
# The upstream canary verifier uses SNS Publish. Do not invoke it.
|
| 73 |
-
extra = row.get("ExtraData")
|
| 74 |
-
if isinstance(extra, dict) and str(extra.get("is_canary", "")).lower() == "true":
|
| 75 |
-
return None
|
| 76 |
-
pair = row.get("RawV2", "")
|
| 77 |
-
if not isinstance(pair, str) or not re.fullmatch(r"(?:AKIA|ABIA|ACCA)[A-Z0-9]{16}:[A-Za-z0-9/+=]{40}", pair):
|
| 78 |
-
return None
|
| 79 |
-
return pair.replace(":", "\n", 1)
|
| 80 |
-
# Preserve the context needed by GitHub's legacy 40-character token detector.
|
| 81 |
-
return ("github_token = " if name == "Github" else "") + raw
|
| 82 |
-
|
| 83 |
|
| 84 |
-
async def verify_trufflehog(rows, root):
|
| 85 |
-
candidates = trufflehog_candidates(rows)
|
| 86 |
-
states = {key: "not_attempted" for key in candidates}
|
| 87 |
-
attempted = 0
|
| 88 |
-
started = time.monotonic()
|
| 89 |
-
directory = root / "verification"
|
| 90 |
-
directory.mkdir(mode=0o700)
|
| 91 |
-
for key, row in candidates.items():
|
| 92 |
-
name, _ = key
|
| 93 |
-
text = verification_source(name, row) if name in TRUFFLEHOG_VERIFIERS else None
|
| 94 |
-
remaining = TRUFFLEHOG_VERIFY_SECONDS - (time.monotonic() - started)
|
| 95 |
-
if text is None or attempted >= TRUFFLEHOG_VERIFY_LIMIT or remaining < 1:
|
| 96 |
-
continue
|
| 97 |
-
path = directory / "candidate.txt"
|
| 98 |
-
path.write_text(text, encoding="utf-8")
|
| 99 |
-
path.chmod(0o600)
|
| 100 |
-
args = ["/usr/local/bin/trufflehog", "filesystem", str(path), "--json",
|
| 101 |
-
"--include-detectors", name, "--results", "verified,unverified,unknown",
|
| 102 |
-
"--allow-verification-overlap", "--no-verification-cache", "--no-update",
|
| 103 |
-
"--concurrency", "1", "--force-skip-binaries", "--force-skip-archives",
|
| 104 |
-
"--fail-on-scan-errors"]
|
| 105 |
-
attempted += 1
|
| 106 |
-
try:
|
| 107 |
-
verified_rows, status = await execute(args, min(12, max(1, int(remaining))))
|
| 108 |
-
except Exception:
|
| 109 |
-
# Executor failures must preserve detection results without persisting exception text.
|
| 110 |
-
verified_rows, status = [], "failed"
|
| 111 |
-
finally:
|
| 112 |
-
path.unlink(missing_ok=True)
|
| 113 |
-
matches = [item for candidate, item in trufflehog_candidates(verified_rows).items() if candidate == key]
|
| 114 |
-
# Missing output, timeout and transport/provider errors never mean invalid.
|
| 115 |
-
state = "unknown"
|
| 116 |
-
if any(item.get("Verified") is True for item in matches):
|
| 117 |
-
state = "verified"
|
| 118 |
-
elif status == "completed" and matches and all(
|
| 119 |
-
item.get("Verified") is False and not item.get("VerificationError") for item in matches):
|
| 120 |
-
state = "unverified"
|
| 121 |
-
states[key] = state
|
| 122 |
-
return states, attempted
|
| 123 |
|
| 124 |
-
|
| 125 |
-
def parse_trufflehog(rows, url, states=None):
|
| 126 |
# Never return Raw, RawV2, Redacted, ExtraData, errors or source snippets.
|
| 127 |
groups = {}
|
| 128 |
-
for
|
| 129 |
-
name, _ = key
|
| 130 |
-
state = (states or {}).get(key, "not_attempted")
|
| 131 |
groups[(name, state)] = groups.get((name, state), 0) + 1
|
| 132 |
descriptions = {
|
| 133 |
-
"verified": "
|
| 134 |
-
"unverified": "TruffleHog did not confirm validity.
|
| 135 |
-
|
| 136 |
-
"
|
|
|
|
| 137 |
}
|
| 138 |
findings = []
|
| 139 |
for (name, state), count in groups.items():
|
|
@@ -227,10 +187,15 @@ async def run(module, url, host, agent, delay, inputs=None, profile="current"):
|
|
| 227 |
(source / "source.txt").write_text(values.source_text, encoding="utf-8")
|
| 228 |
(source / "source.txt").chmod(0o600)
|
| 229 |
args = ["/usr/local/bin/trufflehog", "filesystem", str(source), "--json",
|
| 230 |
-
"--
|
| 231 |
"--force-skip-binaries", "--force-skip-archives", "--fail-on-scan-errors"]
|
| 232 |
-
|
| 233 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
elif module in ("trivy", "depscan", "osv-scanner"):
|
| 235 |
sbom = root / "bom.json"
|
| 236 |
sbom.write_text(values.sbom_text, encoding="utf-8")
|
|
@@ -292,26 +257,17 @@ async def run(module, url, host, agent, delay, inputs=None, profile="current"):
|
|
| 292 |
raise ValueError("Unsupported module")
|
| 293 |
rows, status = await execute(args, deadline)
|
| 294 |
if module == "trufflehog":
|
| 295 |
-
states
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
eligible = sum(name in TRUFFLEHOG_VERIFIERS and verification_source(name, row) is not None
|
| 302 |
-
for (name, _), row in candidates.items())
|
| 303 |
-
verification = {"enabled": values.trufflehog_verify, "attempted": attempted,
|
| 304 |
-
"eligible": eligible,
|
| 305 |
-
"budget_limited": max(0, eligible - attempted) if values.trufflehog_verify else 0,
|
| 306 |
-
"limit": TRUFFLEHOG_VERIFY_LIMIT, "budget_seconds": TRUFFLEHOG_VERIFY_SECONDS,
|
| 307 |
-
**{state: sum(states.get(key, "not_attempted") == state for key in candidates)
|
| 308 |
for state in ("verified", "unverified", "unknown", "not_attempted")}}
|
| 309 |
if values.trufflehog_verify:
|
| 310 |
-
reason =
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
"; values, identity metadata and provider errors redacted")
|
| 314 |
-
if (verification["unknown"] or verification["budget_limited"]) and status == "completed":
|
| 315 |
status = "partial"
|
| 316 |
elif module == "amass":
|
| 317 |
path = root / "hosts.txt"
|
|
|
|
| 4 |
import os
|
| 5 |
import re
|
| 6 |
import tempfile
|
|
|
|
| 7 |
from pathlib import Path
|
| 8 |
from urllib.parse import urljoin, urlsplit
|
| 9 |
|
|
|
|
| 43 |
return found
|
| 44 |
|
| 45 |
|
| 46 |
+
# Let the pinned native tool choose and run its built-in detector verifiers.
|
| 47 |
+
TRUFFLEHOG_DETECTION_SECONDS = 40
|
| 48 |
+
TRUFFLEHOG_VERIFY_SECONDS = 120
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def native_secret_state(row):
|
| 52 |
+
if row.get("Verified") is True:
|
| 53 |
+
return "verified"
|
| 54 |
+
if row.get("VerificationSkipped") is True:
|
| 55 |
+
return "not_attempted"
|
| 56 |
+
if row.get("VerificationError") or not isinstance(row.get("Verified"), bool):
|
| 57 |
+
return "unknown"
|
| 58 |
+
# Native false includes unsupported/skipped verification in versions whose
|
| 59 |
+
# JSON does not expose VerificationSkipped. Never describe it as invalid.
|
| 60 |
+
return "unverified"
|
| 61 |
|
| 62 |
|
| 63 |
def trufflehog_candidates(rows):
|
| 64 |
# Keys and native metadata remain in memory only, never in returned diagnostics.
|
| 65 |
candidates = {}
|
| 66 |
+
rank = {"verified": 3, "unknown": 2, "unverified": 1, "not_attempted": 0}
|
| 67 |
for row in json_rows(rows):
|
| 68 |
name = row.get("DetectorName")
|
| 69 |
if not isinstance(name, str) or not re.fullmatch(r"[A-Za-z][A-Za-z0-9_]{0,79}", name):
|
|
|
|
| 71 |
raw = row.get("RawV2") or row.get("Raw") or ""
|
| 72 |
if not isinstance(raw, str):
|
| 73 |
continue
|
| 74 |
+
key = (name, raw)
|
| 75 |
+
previous = candidates.get(key)
|
| 76 |
+
if previous is None or rank[native_secret_state(row)] > rank[native_secret_state(previous)]:
|
| 77 |
+
candidates[key] = row
|
| 78 |
return candidates
|
| 79 |
|
| 80 |
|
| 81 |
+
def trufflehog_states(rows, verification_enabled):
|
| 82 |
+
return {key: native_secret_state(row) if verification_enabled else "not_attempted"
|
| 83 |
+
for key, row in trufflehog_candidates(rows).items()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
+
def parse_trufflehog(rows, url, verification_enabled=False):
|
|
|
|
| 87 |
# Never return Raw, RawV2, Redacted, ExtraData, errors or source snippets.
|
| 88 |
groups = {}
|
| 89 |
+
for (name, _), state in trufflehog_states(rows, verification_enabled).items():
|
|
|
|
|
|
|
| 90 |
groups[(name, state)] = groups.get((name, state), 0) + 1
|
| 91 |
descriptions = {
|
| 92 |
+
"verified": "Native TruffleHog verification confirmed this credential at scan time.",
|
| 93 |
+
"unverified": ("Native TruffleHog did not confirm validity. The detector may not support "
|
| 94 |
+
"verification or may have skipped it; this does not prove the credential is invalid."),
|
| 95 |
+
"unknown": "Native verification was inconclusive or reported an error; validity is unknown.",
|
| 96 |
+
"not_attempted": "Detection only; verification was disabled or the native detector reported skipping it.",
|
| 97 |
}
|
| 98 |
findings = []
|
| 99 |
for (name, state), count in groups.items():
|
|
|
|
| 187 |
(source / "source.txt").write_text(values.source_text, encoding="utf-8")
|
| 188 |
(source / "source.txt").chmod(0o600)
|
| 189 |
args = ["/usr/local/bin/trufflehog", "filesystem", str(source), "--json",
|
| 190 |
+
"--results", "verified,unverified,unknown", "--no-update", "--concurrency", "1",
|
| 191 |
"--force-skip-binaries", "--force-skip-archives", "--fail-on-scan-errors"]
|
| 192 |
+
if values.trufflehog_verify:
|
| 193 |
+
deadline = TRUFFLEHOG_VERIFY_SECONDS
|
| 194 |
+
reason = "Native TruffleHog verification enabled for all detectors; original supplied source; values and context redacted"
|
| 195 |
+
else:
|
| 196 |
+
args.append("--no-verification")
|
| 197 |
+
deadline = TRUFFLEHOG_DETECTION_SECONDS
|
| 198 |
+
reason = "Supplied source only; secret verification disabled; values and context redacted"
|
| 199 |
elif module in ("trivy", "depscan", "osv-scanner"):
|
| 200 |
sbom = root / "bom.json"
|
| 201 |
sbom.write_text(values.sbom_text, encoding="utf-8")
|
|
|
|
| 257 |
raise ValueError("Unsupported module")
|
| 258 |
rows, status = await execute(args, deadline)
|
| 259 |
if module == "trufflehog":
|
| 260 |
+
states = trufflehog_states(rows, values.trufflehog_verify)
|
| 261 |
+
findings = parse_trufflehog(rows, url, values.trufflehog_verify)
|
| 262 |
+
verification = {"enabled": values.trufflehog_verify, "mode": "native",
|
| 263 |
+
"detectors": "all", "deadline_seconds": deadline,
|
| 264 |
+
"candidates_reported": len(states),
|
| 265 |
+
**{state: sum(value == state for value in states.values())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
for state in ("verified", "unverified", "unknown", "not_attempted")}}
|
| 267 |
if values.trufflehog_verify:
|
| 268 |
+
reason += "; " + ", ".join(f"{verification[state]} {state.replace('_', ' ')}"
|
| 269 |
+
for state in ("verified", "unverified", "unknown", "not_attempted"))
|
| 270 |
+
if verification["unknown"] and status == "completed":
|
|
|
|
|
|
|
| 271 |
status = "partial"
|
| 272 |
elif module == "amass":
|
| 273 |
path = root / "hosts.txt"
|
app/index.html
CHANGED
|
@@ -73,7 +73,7 @@ https://example.org/,Main app</pre><small>TSV uses tabs. Supported target header
|
|
| 73 |
<label class="module-card" data-module-search="dalfox unverified xss review."><input id="modDalfox" type="checkbox" onchange="updatePipelinePreview()"/><span><strong>Dalfox</strong><small>Unverified XSS review.</small></span></label>
|
| 74 |
<label class="module-card" data-module-search="sqlmap low-risk boolean tests; no data extraction."><input id="modSqlmap" type="checkbox" onchange="updatePipelinePreview()"/><span><strong>SQLmap</strong><small>Low-risk boolean tests; no data extraction.</small></span></label>
|
| 75 |
<label class="module-card" data-module-search="garak two benign output-filter checks; rest llm path required."><input id="modGarak" type="checkbox" onchange="updatePipelinePreview()"/><span><strong>garak</strong><small>Two benign output-filter checks; REST LLM path required.</small></span></label></div></section>
|
| 76 |
-
<section class="module-group"><h3>Source & dependencies</h3><div class="module-grid"><label class="module-card" data-module-search="trufflehog supplied source text; optional
|
| 77 |
<label class="module-card" data-module-search="dep-scan cyclonedx sbom; application-only database, 2024 onward."><input id="modDepscan" type="checkbox" onchange="updatePipelinePreview()"/><span><strong>dep-scan</strong><small>CycloneDX SBOM; application-only database, 2024 onward.</small></span></label>
|
| 78 |
<label class="module-card" data-module-search="trivy cyclonedx sbom; keyless advisory database."><input id="modTrivy" type="checkbox" onchange="updatePipelinePreview()"/><span><strong>Trivy</strong><small>CycloneDX SBOM; keyless advisory database.</small></span></label>
|
| 79 |
<label class="module-card" data-module-search="osv-scanner cyclonedx sbom; public advisory service."><input id="modOsv" type="checkbox" onchange="updatePipelinePreview()"/><span><strong>OSV-Scanner</strong><small>CycloneDX SBOM; public advisory service.</small></span></label></div></section><p id="moduleSearchEmpty" class="empty" hidden>No matching modules.</p></section>
|
|
@@ -88,7 +88,7 @@ https://example.org/,Main app</pre><small>TSV uses tabs. Supported target header
|
|
| 88 |
<div class="settings-group" id="settings-inputs" hidden><section class="card"><span class="eyebrow">Query reference comparisons</span><h2>IDOR-Forge · limited GET profile</h2><p class="muted">Use explicit reference IDs you are authorized to test. This keyless, anonymous profile sends at most four GET requests to one path and never follows redirects. Configure one selected asset at a time. Without these inputs the module skips.</p>
|
| 89 |
<div class="toolbar"><div class="field"><label for="moduleIdorPath">Same-origin endpoint path</label><input id="moduleIdorPath" maxlength="250" placeholder="/api/document"/></div><div class="field"><label for="moduleIdorParameter">Query parameter</label><input id="moduleIdorParameter" maxlength="50" value="id"/></div></div>
|
| 90 |
<div class="toolbar"><div class="field"><label for="moduleIdorBaseline">Baseline reference ID</label><input id="moduleIdorBaseline" maxlength="100" autocomplete="off" placeholder="1001"/></div><div class="field"><label for="moduleIdorValues">Reference IDs to compare (up to 3, comma-separated)</label><input id="moduleIdorValues" maxlength="302" autocomplete="off" placeholder="1002, 1003"/></div></div>
|
| 91 |
-
<p class="notice">Similarity is a review signal. This profile cannot prove object ownership or missing authorization; verify candidates using two authorized test accounts. Credentials, mutations, broad payload fuzzing and the upstream GUI are outside this profile. Reference values and response bodies are omitted from results and logs.</p></section><section class="card"><h2>Optional module inputs</h2><p class="muted">Optional inputs stay in this job's memory. Source/SBOM content is not included in saved results. Attach artifacts to one selected asset at a time. Full pipeline skips tools whose inputs are absent.</p><div class="field"><label for="moduleSource">Source text for TruffleHog (max 200,000 characters)</label><textarea id="moduleSource" maxlength="200000" rows="4" placeholder="Paste source or configuration text to inspect. Detected values and context are redacted."></textarea></div><p><label><input id="moduleTrufflehogVerify" type="checkbox"/> Verify found credentials I own or am authorized to test</label></p><p class="muted">Optional:
|
| 92 |
<div class="settings-group" id="settings-nuclei" hidden><section class="card" id="nucleiSettings"><h2>Nuclei · Template selection</h2>
|
| 93 |
<p class="muted">Combine any bundled profiles, compatible community templates, and custom YAML. This selection applies to new scans only. Community and custom templates must pass the server's GET/HEAD-only validation; dynamic DSL, POST requests, remote interactions, and unsafe paths are blocked.</p>
|
| 94 |
<div class="grid">
|
|
|
|
| 73 |
<label class="module-card" data-module-search="dalfox unverified xss review."><input id="modDalfox" type="checkbox" onchange="updatePipelinePreview()"/><span><strong>Dalfox</strong><small>Unverified XSS review.</small></span></label>
|
| 74 |
<label class="module-card" data-module-search="sqlmap low-risk boolean tests; no data extraction."><input id="modSqlmap" type="checkbox" onchange="updatePipelinePreview()"/><span><strong>SQLmap</strong><small>Low-risk boolean tests; no data extraction.</small></span></label>
|
| 75 |
<label class="module-card" data-module-search="garak two benign output-filter checks; rest llm path required."><input id="modGarak" type="checkbox" onchange="updatePipelinePreview()"/><span><strong>garak</strong><small>Two benign output-filter checks; REST LLM path required.</small></span></label></div></section>
|
| 76 |
+
<section class="module-group"><h3>Source & dependencies</h3><div class="module-grid"><label class="module-card" data-module-search="trufflehog supplied source text; optional native credential verification."><input id="modTrufflehog" type="checkbox" onchange="updatePipelinePreview()"/><span><strong>TruffleHog</strong><small>Supplied source text; optional native credential verification.</small></span></label>
|
| 77 |
<label class="module-card" data-module-search="dep-scan cyclonedx sbom; application-only database, 2024 onward."><input id="modDepscan" type="checkbox" onchange="updatePipelinePreview()"/><span><strong>dep-scan</strong><small>CycloneDX SBOM; application-only database, 2024 onward.</small></span></label>
|
| 78 |
<label class="module-card" data-module-search="trivy cyclonedx sbom; keyless advisory database."><input id="modTrivy" type="checkbox" onchange="updatePipelinePreview()"/><span><strong>Trivy</strong><small>CycloneDX SBOM; keyless advisory database.</small></span></label>
|
| 79 |
<label class="module-card" data-module-search="osv-scanner cyclonedx sbom; public advisory service."><input id="modOsv" type="checkbox" onchange="updatePipelinePreview()"/><span><strong>OSV-Scanner</strong><small>CycloneDX SBOM; public advisory service.</small></span></label></div></section><p id="moduleSearchEmpty" class="empty" hidden>No matching modules.</p></section>
|
|
|
|
| 88 |
<div class="settings-group" id="settings-inputs" hidden><section class="card"><span class="eyebrow">Query reference comparisons</span><h2>IDOR-Forge · limited GET profile</h2><p class="muted">Use explicit reference IDs you are authorized to test. This keyless, anonymous profile sends at most four GET requests to one path and never follows redirects. Configure one selected asset at a time. Without these inputs the module skips.</p>
|
| 89 |
<div class="toolbar"><div class="field"><label for="moduleIdorPath">Same-origin endpoint path</label><input id="moduleIdorPath" maxlength="250" placeholder="/api/document"/></div><div class="field"><label for="moduleIdorParameter">Query parameter</label><input id="moduleIdorParameter" maxlength="50" value="id"/></div></div>
|
| 90 |
<div class="toolbar"><div class="field"><label for="moduleIdorBaseline">Baseline reference ID</label><input id="moduleIdorBaseline" maxlength="100" autocomplete="off" placeholder="1001"/></div><div class="field"><label for="moduleIdorValues">Reference IDs to compare (up to 3, comma-separated)</label><input id="moduleIdorValues" maxlength="302" autocomplete="off" placeholder="1002, 1003"/></div></div>
|
| 91 |
+
<p class="notice">Similarity is a review signal. This profile cannot prove object ownership or missing authorization; verify candidates using two authorized test accounts. Credentials, mutations, broad payload fuzzing and the upstream GUI are outside this profile. Reference values and response bodies are omitted from results and logs.</p></section><section class="card"><h2>Optional module inputs</h2><p class="muted">Optional inputs stay in this job's memory. Source/SBOM content is not included in saved results. Attach artifacts to one selected asset at a time. Full pipeline skips tools whose inputs are absent.</p><div class="field"><label for="moduleSource">Source text for TruffleHog (max 200,000 characters)</label><textarea id="moduleSource" maxlength="200000" rows="4" placeholder="Paste source or configuration text to inspect. Detected values and context are redacted."></textarea></div><p><label><input id="moduleTrufflehogVerify" type="checkbox"/> Verify found credentials I own or am authorized to test</label></p><p class="muted">Optional: runs TruffleHog’s built-in verification for all detector types against the original supplied source. Native detectors use the credential and its source context to check the relevant service, which can include a database or another host. No five-provider restriction or 10-candidate cap. One worker, with a 120-second overall process limit on HF. Some detectors may be unable to verify, skip verification or encounter network errors; an unverified result never proves a secret is invalid. Values, account details and provider errors are redacted. Full pipeline does not enable this option.</p><div class="field"><label for="moduleSbom">CycloneDX SBOM JSON for dep-scan, Trivy and OSV-Scanner (max 300 components)</label><textarea id="moduleSbom" maxlength="1000000" rows="4" placeholder='{"bomFormat":"CycloneDX","components":[{"name":"package","version":"1.0.0","purl":"pkg:npm/package@1.0.0"}]}'></textarea></div><p class="muted">These tools inspect supplied package metadata. OSV sends package names/versions to its public advisory service. Trivy and dep-scan fetch public databases on their first applicable run; database availability is shown in logs. dep-scan uses a smaller application-only database covering 2024 onward.</p><p><label><input id="moduleApiTarget" type="checkbox"/> This asset is an API target; allow Kiterunner's eight-route GET sample.</label></p><p class="muted">Amass uses standalone 4.2.0 passive discovery. Returned hostnames are inventory only and are not added to scan scope. Nettacker runs one HTTP-status check.</p><div class="toolbar"><div class="field"><label for="moduleLlmPath">Optional same-origin REST LLM path for garak</label><input id="moduleLlmPath" placeholder="/api/generate" maxlength="250"/></div><div class="field"><label>Top-level request prompt field</label><input id="modulePromptField" value="prompt" maxlength="50"/></div><div class="field"><label>Top-level response text field</label><input id="moduleResponseField" value="response" maxlength="50"/></div></div><p class="muted">garak runs two benign antivirus-signature output-filter checks. Use this only for an LLM expected to filter those signatures. No local model, API key, browser or GPU is required.</p></section></div>
|
| 92 |
<div class="settings-group" id="settings-nuclei" hidden><section class="card" id="nucleiSettings"><h2>Nuclei · Template selection</h2>
|
| 93 |
<p class="muted">Combine any bundled profiles, compatible community templates, and custom YAML. This selection applies to new scans only. Community and custom templates must pass the server's GET/HEAD-only validation; dynamic DSL, POST requests, remote interactions, and unsafe paths are blocked.</p>
|
| 94 |
<div class="grid">
|
tests/test_final_modules.py
CHANGED
|
@@ -201,143 +201,131 @@ def test_draft_endpoint_validates_but_never_launches_scan():
|
|
| 201 |
assert response.json()["executed"] is False
|
| 202 |
|
| 203 |
|
| 204 |
-
#
|
| 205 |
-
def
|
| 206 |
-
|
| 207 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
async def execute(args, timeout):
|
| 209 |
calls.append((args, timeout))
|
| 210 |
assert "--no-update" in args and "--force-skip-archives" in args
|
| 211 |
-
|
| 212 |
-
if "--no-verification" in args:
|
| 213 |
-
return rows, "completed"
|
| 214 |
-
assert "--include-detectors" in args and "--allow-verification-overlap" in args
|
| 215 |
-
assert "--no-verification-cache" in args
|
| 216 |
assert args[args.index("--results") + 1] == "verified,unverified,unknown"
|
| 217 |
-
assert
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
assert "fixture-private" not in " ".join(args)
|
| 219 |
paths.append(path)
|
| 220 |
-
return
|
| 221 |
with patch.object(modules, "execute", execute):
|
| 222 |
result = asyncio.run(modules.run("trufflehog", "https://example.org/", "example.org", "Test", 1.5,
|
| 223 |
-
ModuleInputs(source_text=
|
| 224 |
assert all(not path.exists() for path in paths)
|
|
|
|
| 225 |
assert "fixture-private" not in json.dumps(result)
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
def native_secret(name="Github", raw="fixture-private-key", **fields):
|
| 230 |
-
return json.dumps({"DetectorName": name, "Raw": raw, "Redacted": raw,
|
| 231 |
-
"ExtraData": {"account": "fixture-private-identity"}, **fields})
|
| 232 |
|
| 233 |
|
| 234 |
@pytest.mark.parametrize("fields,status,expected", [
|
| 235 |
({"Verified": True}, "completed", "verified"),
|
| 236 |
({"Verified": False}, "completed", "unverified"),
|
| 237 |
({"Verified": False, "VerificationError": "fixture-private-provider-error"}, "completed", "unknown"),
|
| 238 |
-
({"Verified": False}, "timeout", "
|
| 239 |
({"Verified": True}, "timeout", "verified"),
|
|
|
|
| 240 |
({}, "completed", "unknown"),
|
| 241 |
])
|
| 242 |
-
def
|
| 243 |
-
|
| 244 |
-
assert path.read_text() == "github_token = fixture-private-key"
|
| 245 |
-
assert args[args.index("--include-detectors") + 1] == "Github"
|
| 246 |
-
return [native_secret(**fields)], status
|
| 247 |
-
result, calls = run_secret_fixture([native_secret()], reply)
|
| 248 |
finding = result["alerts"][0]
|
| 249 |
-
assert len(calls) == 2 and result["verification"]["attempted"] == 1
|
| 250 |
assert finding["verification_status"] == expected
|
| 251 |
assert finding["confidence"] == ("High" if expected == "verified" else "Low")
|
| 252 |
assert finding["risk"] == ("High" if expected == "verified" else "Medium")
|
| 253 |
-
assert result["
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
|
| 255 |
|
| 256 |
-
def
|
| 257 |
-
result
|
| 258 |
-
assert len(calls) == 1
|
| 259 |
assert result["alerts"][0]["verification_status"] == "not_attempted"
|
| 260 |
assert result["verification"]["enabled"] is False
|
| 261 |
|
| 262 |
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
assert
|
| 269 |
-
assert result["verification"]["not_attempted"] == 3
|
| 270 |
|
| 271 |
|
| 272 |
-
def
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
assert result["verification"]["
|
| 276 |
-
assert result["verification"]["
|
|
|
|
| 277 |
|
| 278 |
|
| 279 |
-
def
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
assert result["status"] == "
|
| 284 |
-
assert result["verification"]["
|
|
|
|
| 285 |
|
| 286 |
|
| 287 |
-
def
|
| 288 |
-
|
| 289 |
-
result
|
| 290 |
-
assert
|
| 291 |
-
assert result["verification"]["attempted"] == 10
|
| 292 |
-
assert result["verification"]["unknown"] == 10
|
| 293 |
-
assert result["verification"]["not_attempted"] == 2
|
| 294 |
-
assert result["verification"]["budget_limited"] == 2
|
| 295 |
-
assert sum(item["candidate_count"] for item in result["alerts"]) == 12
|
| 296 |
|
| 297 |
|
| 298 |
-
def
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
assert
|
| 311 |
-
assert
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
def
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
def reply(args, path):
|
| 321 |
-
assert args[args.index("--include-detectors") + 1] == name
|
| 322 |
-
assert "--verifier" not in args and "--config" not in args
|
| 323 |
-
if name == "AWS":
|
| 324 |
-
assert path.read_text() == pair.replace(":", "\n")
|
| 325 |
-
return [], "completed"
|
| 326 |
-
result, calls = run_secret_fixture([row], reply)
|
| 327 |
-
assert len(calls) == 2 and result["verification"]["unknown"] == 1
|
| 328 |
|
| 329 |
|
| 330 |
def test_verification_flag_is_visible_but_source_stays_private_in_job_options():
|
| 331 |
summary = ModuleInputs(source_text="fixture-private-source", trufflehog_verify=True).public_summary()
|
| 332 |
assert summary["trufflehog_verify"] is True
|
| 333 |
assert "fixture-private" not in json.dumps(summary)
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
def test_multiple_candidates_of_one_detector_keep_separate_verification_results():
|
| 337 |
-
rows = [native_secret(raw="fixture-private-good"), native_secret(raw="fixture-private-bad")]
|
| 338 |
-
def reply(args, path):
|
| 339 |
-
raw = path.read_text().split(" = ", 1)[1]
|
| 340 |
-
return [native_secret(raw=raw, Verified=raw.endswith("good"))], "completed"
|
| 341 |
-
result, _ = run_secret_fixture(rows, reply)
|
| 342 |
-
assert result["verification"]["verified"] == result["verification"]["unverified"] == 1
|
| 343 |
-
assert {a["verification_status"] for a in result["alerts"]} == {"verified", "unverified"}
|
|
|
|
| 201 |
assert response.json()["executed"] is False
|
| 202 |
|
| 203 |
|
| 204 |
+
# Native process output is mocked. No test authenticates a real credential.
|
| 205 |
+
def native_secret(name="Github", raw="fixture-private-key", **fields):
|
| 206 |
+
return json.dumps({"DetectorName": name, "Raw": raw, "Redacted": raw,
|
| 207 |
+
"ExtraData": {"account": "fixture-private-identity"},
|
| 208 |
+
"SourceMetadata": {"source": "fixture-private-source"},
|
| 209 |
+
"SecretParts": {"key": raw}, **fields})
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def run_secret_fixture(rows, enabled=True, native_status="completed"):
|
| 213 |
+
calls, paths = [], []
|
| 214 |
+
source = "database=https://fixture-private-host/\npassword=fixture-private-key\noriginal context"
|
| 215 |
async def execute(args, timeout):
|
| 216 |
calls.append((args, timeout))
|
| 217 |
assert "--no-update" in args and "--force-skip-archives" in args
|
| 218 |
+
assert "--force-skip-binaries" in args
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
assert args[args.index("--results") + 1] == "verified,unverified,unknown"
|
| 220 |
+
assert args[args.index("--concurrency") + 1] == "1"
|
| 221 |
+
# No allowlist, source reconstruction or replacement of built-in policy.
|
| 222 |
+
assert not {"--include-detectors", "--exclude-detectors", "--verifier", "--config",
|
| 223 |
+
"--allow-verification-overlap", "--no-verification-cache"} & set(args)
|
| 224 |
+
assert ("--no-verification" in args) is (not enabled)
|
| 225 |
+
assert timeout == (120 if enabled else 40)
|
| 226 |
+
root = Path(args[args.index("filesystem") + 1])
|
| 227 |
+
path = root / "source.txt"
|
| 228 |
+
assert path.read_text() == source
|
| 229 |
+
assert (path.stat().st_mode & 0o777) == 0o600
|
| 230 |
+
assert sorted(p.name for p in root.iterdir()) == ["source.txt"]
|
| 231 |
assert "fixture-private" not in " ".join(args)
|
| 232 |
paths.append(path)
|
| 233 |
+
return rows, native_status
|
| 234 |
with patch.object(modules, "execute", execute):
|
| 235 |
result = asyncio.run(modules.run("trufflehog", "https://example.org/", "example.org", "Test", 1.5,
|
| 236 |
+
ModuleInputs(source_text=source, trufflehog_verify=enabled)))
|
| 237 |
assert all(not path.exists() for path in paths)
|
| 238 |
+
assert len(calls) == 1
|
| 239 |
assert "fixture-private" not in json.dumps(result)
|
| 240 |
+
assert "attempted" not in result["verification"]
|
| 241 |
+
assert "limit" not in result["verification"]
|
| 242 |
+
return result
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
|
| 245 |
@pytest.mark.parametrize("fields,status,expected", [
|
| 246 |
({"Verified": True}, "completed", "verified"),
|
| 247 |
({"Verified": False}, "completed", "unverified"),
|
| 248 |
({"Verified": False, "VerificationError": "fixture-private-provider-error"}, "completed", "unknown"),
|
| 249 |
+
({"Verified": False}, "timeout", "unverified"),
|
| 250 |
({"Verified": True}, "timeout", "verified"),
|
| 251 |
+
({"Verified": False, "VerificationSkipped": True}, "completed", "not_attempted"),
|
| 252 |
({}, "completed", "unknown"),
|
| 253 |
])
|
| 254 |
+
def test_native_verification_labels_and_redaction(fields, status, expected):
|
| 255 |
+
result = run_secret_fixture([native_secret(**fields)], native_status=status)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
finding = result["alerts"][0]
|
|
|
|
| 257 |
assert finding["verification_status"] == expected
|
| 258 |
assert finding["confidence"] == ("High" if expected == "verified" else "Low")
|
| 259 |
assert finding["risk"] == ("High" if expected == "verified" else "Medium")
|
| 260 |
+
assert result["verification"][expected] == 1
|
| 261 |
+
assert result["verification"]["mode"] == "native"
|
| 262 |
+
assert result["verification"]["detectors"] == "all"
|
| 263 |
+
assert result["status"] == (status if status != "completed" else
|
| 264 |
+
"partial" if expected == "unknown" else "findings")
|
| 265 |
|
| 266 |
|
| 267 |
+
def test_detection_default_disables_native_verification_even_if_output_claims_verified():
|
| 268 |
+
result = run_secret_fixture([native_secret(Verified=True)], enabled=False)
|
|
|
|
| 269 |
assert result["alerts"][0]["verification_status"] == "not_attempted"
|
| 270 |
assert result["verification"]["enabled"] is False
|
| 271 |
|
| 272 |
|
| 273 |
+
@pytest.mark.parametrize("name", ["AWS", "Github", "Slack", "HuggingFace", "OpenAI",
|
| 274 |
+
"Stripe", "Postgres", "Azure", "PrivateKey"])
|
| 275 |
+
def test_native_verification_accepts_all_detector_results_without_application_allowlist(name):
|
| 276 |
+
result = run_secret_fixture([native_secret(name, Verified=True)])
|
| 277 |
+
assert result["verification"]["verified"] == 1
|
| 278 |
+
assert result["alerts"][0]["name"] == "Verified " + name + " secret"
|
|
|
|
| 279 |
|
| 280 |
|
| 281 |
+
def test_more_than_ten_credentials_remain_in_single_native_scan():
|
| 282 |
+
rows = [native_secret(raw="fixture-private-" + str(i), Verified=True) for i in range(15)]
|
| 283 |
+
result = run_secret_fixture(rows)
|
| 284 |
+
assert result["verification"]["verified"] == 15
|
| 285 |
+
assert result["verification"]["candidates_reported"] == 15
|
| 286 |
+
assert result["alerts"][0]["candidate_count"] == 15
|
| 287 |
|
| 288 |
|
| 289 |
+
def test_partial_native_output_retains_findings_and_timeout_status():
|
| 290 |
+
result = run_secret_fixture([native_secret(Verified=True),
|
| 291 |
+
"[diagnostic] Deadline exceeded; fixture-private-error"],
|
| 292 |
+
native_status="timeout")
|
| 293 |
+
assert result["status"] == "timeout"
|
| 294 |
+
assert result["verification"]["verified"] == 1
|
| 295 |
+
assert result["alerts"][0]["verification_status"] == "verified"
|
| 296 |
|
| 297 |
|
| 298 |
+
def test_empty_timeout_cannot_claim_clean_scan_or_completed_verification():
|
| 299 |
+
result = run_secret_fixture(["[diagnostic] Deadline exceeded"], native_status="timeout")
|
| 300 |
+
assert result["status"] == "timeout" and result["alerts"] == []
|
| 301 |
+
assert result["verification"]["candidates_reported"] == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
|
| 303 |
|
| 304 |
+
def test_unverified_does_not_claim_invalid_or_known_verification_attempt():
|
| 305 |
+
result = run_secret_fixture([native_secret("UnsupportedExample", Verified=False)])
|
| 306 |
+
assert result["alerts"][0]["verification_status"] == "unverified"
|
| 307 |
+
assert "may not support" in result["alerts"][0]["description"]
|
| 308 |
+
assert "does not prove" in result["alerts"][0]["description"]
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def test_native_verification_deduplicates_candidates_and_keeps_strongest_evidence():
|
| 312 |
+
rows = [native_secret(raw="fixture-private-good", Verified=False),
|
| 313 |
+
native_secret(raw="fixture-private-good", Verified=True),
|
| 314 |
+
native_secret(raw="fixture-private-bad", Verified=False)]
|
| 315 |
+
result = run_secret_fixture(rows)
|
| 316 |
+
assert result["verification"]["verified"] == result["verification"]["unverified"] == 1
|
| 317 |
+
assert result["verification"]["candidates_reported"] == 2
|
| 318 |
+
assert {a["verification_status"] for a in result["alerts"]} == {"verified", "unverified"}
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def test_native_verification_summary_counts_all_candidates_beyond_alert_group_cap():
|
| 322 |
+
rows = [native_secret("Detector" + str(i), Verified=True) for i in range(101)]
|
| 323 |
+
result = run_secret_fixture(rows)
|
| 324 |
+
assert result["verification"]["verified"] == 101
|
| 325 |
+
assert len(result["alerts"]) == 100
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
|
| 327 |
|
| 328 |
def test_verification_flag_is_visible_but_source_stays_private_in_job_options():
|
| 329 |
summary = ModuleInputs(source_text="fixture-private-source", trufflehog_verify=True).public_summary()
|
| 330 |
assert summary["trufflehog_verify"] is True
|
| 331 |
assert "fixture-private" not in json.dumps(summary)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/trufflehog_offline_smoke.py
CHANGED
|
@@ -10,9 +10,7 @@ with tempfile.TemporaryDirectory() as tmp:
|
|
| 10 |
"-out", str(key)], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 11 |
result = subprocess.run(["/usr/local/bin/trufflehog", "filesystem", tmp, "--json",
|
| 12 |
"--no-verification", "--no-update", "--concurrency", "1",
|
| 13 |
-
"--
|
| 14 |
-
"--results", "verified,unverified,unknown", "--allow-verification-overlap",
|
| 15 |
-
"--no-verification-cache"], capture_output=True, text=True, timeout=30)
|
| 16 |
assert result.returncode == 0
|
| 17 |
findings = [json.loads(line) for line in result.stdout.splitlines() if line.startswith("{")]
|
| 18 |
assert any("private" in str(item.get("DetectorName", "")).lower() for item in findings)
|
|
|
|
| 10 |
"-out", str(key)], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 11 |
result = subprocess.run(["/usr/local/bin/trufflehog", "filesystem", tmp, "--json",
|
| 12 |
"--no-verification", "--no-update", "--concurrency", "1",
|
| 13 |
+
"--results", "verified,unverified,unknown"], capture_output=True, text=True, timeout=30)
|
|
|
|
|
|
|
| 14 |
assert result.returncode == 0
|
| 15 |
findings = [json.loads(line) for line in result.stdout.splitlines() if line.startswith("{")]
|
| 16 |
assert any("private" in str(item.get("DetectorName", "")).lower() for item in findings)
|