Upload files to "tests"
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import io
|
||||
import unittest
|
||||
from contextlib import redirect_stderr
|
||||
from unittest.mock import patch
|
||||
|
||||
from redflare.cli import main
|
||||
from redflare.interactive import interactive_arguments
|
||||
|
||||
|
||||
class CLITests(unittest.TestCase):
|
||||
def test_scan_requires_authorization_acknowledgement(self):
|
||||
error = io.StringIO()
|
||||
with redirect_stderr(error):
|
||||
code = main(["scan", "http://127.0.0.1"])
|
||||
self.assertEqual(code, 2)
|
||||
self.assertIn("--authorized", error.getvalue())
|
||||
|
||||
@patch(
|
||||
"builtins.input",
|
||||
side_effect=[
|
||||
"1", # full profile
|
||||
"1", # direct targets
|
||||
"https://example.test", # target
|
||||
"", # no scope file
|
||||
"yes", # authorized
|
||||
"no", # not public
|
||||
"yes", # full interaction permitted
|
||||
"runs", # output
|
||||
"1", # workers
|
||||
"10", # timeout
|
||||
"", # no wordlist
|
||||
"1", # rate
|
||||
"25", # max paths
|
||||
"30", # max crawl pages
|
||||
"2", # max crawl depth
|
||||
"no", # no GraphQL introspection
|
||||
"75", # max exposure endpoints
|
||||
"https://github.com/o/r", # repository
|
||||
],
|
||||
)
|
||||
def test_interactive_wizard_builds_full_pipeline(self, _):
|
||||
arguments = interactive_arguments()
|
||||
self.assertIn("full", arguments)
|
||||
self.assertIn("--authorized", arguments)
|
||||
self.assertIn("--github-repo", arguments)
|
||||
self.assertIn("https://example.test", arguments)
|
||||
|
||||
@patch("redflare.interactive.recent_runs", return_value=[])
|
||||
@patch("builtins.input", side_effect=["4", "file:///tmp/run_fixture", "8765", "yes"])
|
||||
def test_interactive_menu_builds_visualizer_command(self, _, __):
|
||||
arguments = interactive_arguments()
|
||||
self.assertEqual(arguments[:2], ["visualize", "file:///tmp/run_fixture"])
|
||||
self.assertNotIn("--no-browser", arguments)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,129 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import io
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
from redflare.core.models import Target
|
||||
from redflare.core.standards import enrich_finding
|
||||
from redflare.core.storage import RunStore
|
||||
from redflare.modules.base import ModuleContext
|
||||
from redflare.modules.exposure import SensitiveExposureModule
|
||||
from redflare.ui import LiveConsole
|
||||
|
||||
|
||||
FAKE_TOKEN = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||
FAKE_HASH = "$2b$12$" + "A" * 53
|
||||
|
||||
|
||||
class ExposureFixture(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path == "/":
|
||||
body = b'<html><script src="/app.js"></script></html>'
|
||||
content_type = "text/html"
|
||||
status = 200
|
||||
elif self.path == "/app.js":
|
||||
body = (
|
||||
f'const api_key = "{FAKE_TOKEN}";\n'
|
||||
f'const password_hash = "{FAKE_HASH}";\n'
|
||||
'const backend = "10.20.30.40";\n'
|
||||
).encode()
|
||||
content_type = "application/javascript"
|
||||
status = 200
|
||||
else:
|
||||
body = b"not found"
|
||||
content_type = "text/plain"
|
||||
status = 404
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", content_type)
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, *_):
|
||||
return
|
||||
|
||||
|
||||
class ExposureTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.server = ThreadingHTTPServer(("127.0.0.1", 0), ExposureFixture)
|
||||
cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True)
|
||||
cls.thread.start()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.server.shutdown()
|
||||
cls.server.server_close()
|
||||
|
||||
def test_detects_and_redacts_sensitive_values(self):
|
||||
text = f'api_key="{FAKE_TOKEN}"; password_hash="{FAKE_HASH}"; host="10.20.30.40"'
|
||||
matches = SensitiveExposureModule.detect(text, "https://example.test/app.js")
|
||||
types = {item["type"] for item in matches}
|
||||
self.assertIn("GitHub token", types)
|
||||
self.assertIn("bcrypt password hash", types)
|
||||
self.assertIn("private/internal IP address", types)
|
||||
serialized = json.dumps(matches)
|
||||
self.assertNotIn(FAKE_TOKEN, serialized)
|
||||
self.assertNotIn(FAKE_HASH, serialized)
|
||||
self.assertIn("10.20.30.40", serialized)
|
||||
|
||||
def test_detects_labeled_public_ip_without_flagging_documentation_ip(self):
|
||||
matches = SensitiveExposureModule.detect(
|
||||
'client_ip="8.8.8.8"; client_ip="192.0.2.10"',
|
||||
"https://example.test/profile",
|
||||
)
|
||||
previews = {item["value_preview"] for item in matches}
|
||||
self.assertIn("8.8.8.8", previews)
|
||||
self.assertNotIn("192.0.2.10", previews)
|
||||
|
||||
def test_scans_graph_endpoints_and_writes_masked_artifact(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
url = f"http://127.0.0.1:{self.server.server_port}"
|
||||
target = Target(url, "127.0.0.1", "http", self.server.server_port)
|
||||
messages = []
|
||||
context = ModuleContext(
|
||||
"exposure-test",
|
||||
Path(directory),
|
||||
timeout=2,
|
||||
reporter=lambda *items: messages.append(items),
|
||||
)
|
||||
context.surface_graph.add_endpoint(
|
||||
target.url, f"{url}/app.js", method="GET", source="test"
|
||||
)
|
||||
result = SensitiveExposureModule().run(target, context)
|
||||
for finding in result.findings:
|
||||
enrich_finding(finding)
|
||||
self.assertGreaterEqual(len(result.findings), 3)
|
||||
self.assertTrue(any(item[2] == "finding" for item in messages))
|
||||
self.assertTrue(all(item.test_id == "RFV2-DATA-001" for item in result.findings))
|
||||
artifact = Path(result.artifacts[0])
|
||||
self.assertTrue(artifact.exists())
|
||||
saved = artifact.read_text(encoding="utf-8")
|
||||
self.assertNotIn(FAKE_TOKEN, saved)
|
||||
self.assertNotIn(FAKE_HASH, saved)
|
||||
self.assertIn("10.20.30.40", saved)
|
||||
|
||||
store = RunStore(directory, "report-test")
|
||||
store.write_result(result)
|
||||
summary = store.finalize([result], surface_graph=context.surface_graph.snapshot())
|
||||
self.assertEqual(summary["sensitive_exposures"], len(result.findings))
|
||||
report = (store.root / "report.html").read_text(encoding="utf-8")
|
||||
jsonl = (store.root / "findings.jsonl").read_text(encoding="utf-8")
|
||||
self.assertIn("RFV2-DATA-001", report)
|
||||
self.assertIn("10.20.30.40", report)
|
||||
self.assertNotIn(FAKE_TOKEN, report + jsonl)
|
||||
terminal = io.StringIO()
|
||||
LiveConsole(stream=terminal).final_report(summary, [result], store.root)
|
||||
terminal_output = terminal.getvalue()
|
||||
self.assertIn("RFV2-DATA-001", terminal_output)
|
||||
self.assertIn("10.20.30.40", terminal_output)
|
||||
self.assertNotIn(FAKE_TOKEN, terminal_output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,43 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from redflare.core.models import Finding
|
||||
from redflare.core.models import Target
|
||||
from redflare.core.storage import RunStore, deduplicate_findings, target_run_id
|
||||
|
||||
|
||||
class StorageTests(unittest.TestCase):
|
||||
def test_target_run_name_is_human_readable(self):
|
||||
value = target_run_id([Target("https://example.test", "example.test", "https", 443)])
|
||||
self.assertRegex(value, r"^scan_example\.test_\d{8}_\d{6}$")
|
||||
|
||||
def test_run_folders_do_not_collide(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
first = RunStore(directory, "run_test")
|
||||
second = RunStore(directory, "run_test")
|
||||
self.assertNotEqual(first.root, second.root)
|
||||
self.assertTrue(first.root.is_dir())
|
||||
self.assertTrue(second.root.is_dir())
|
||||
|
||||
def test_corroborated_header_findings_are_deduplicated(self):
|
||||
common = {
|
||||
"run_id": "run_test",
|
||||
"target": "https://example.test",
|
||||
"severity": "low",
|
||||
"confidence": 0.95,
|
||||
"description": "missing headers",
|
||||
}
|
||||
findings = [
|
||||
Finding(module="http_headers", category="security-headers", title="Missing headers", **common),
|
||||
Finding(module="gatekeeper", category="browser-security-headers", title="Browser missing headers", **common),
|
||||
]
|
||||
deduplicated = deduplicate_findings(findings)
|
||||
self.assertEqual(len(deduplicated), 1)
|
||||
self.assertEqual(
|
||||
deduplicated[0].evidence["corroborated_by"],
|
||||
["gatekeeper", "http_headers"],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,130 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
from importlib.resources import files
|
||||
from pathlib import Path
|
||||
from urllib.request import urlopen
|
||||
|
||||
from redflare.visualize import VisualServer, build_visual_graph, resolve_run_directory
|
||||
|
||||
|
||||
class VisualizeTests(unittest.TestCase):
|
||||
def make_run(self, directory: str) -> Path:
|
||||
root = Path(directory) / "run_visual"
|
||||
root.mkdir()
|
||||
(root / "summary.json").write_text(
|
||||
json.dumps({"run_id": "run_visual", "findings": 1}), encoding="utf-8"
|
||||
)
|
||||
(root / "attack_surface.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"summary": {"targets": 1},
|
||||
"targets": {
|
||||
"https://example.test": {
|
||||
"endpoints": [
|
||||
{
|
||||
"url": "https://example.test/api/users",
|
||||
"methods": ["GET"],
|
||||
"parameters": [
|
||||
{"name": "id", "location": "query", "required": True}
|
||||
],
|
||||
"sources": ["javascript-route"],
|
||||
}
|
||||
],
|
||||
"edges": [],
|
||||
"documents": [
|
||||
{"kind": "openapi", "title": "Fixture API", "url": "https://example.test/openapi.json"}
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
finding = {
|
||||
"id": "finding123",
|
||||
"target": "https://example.test",
|
||||
"module": "sensitive_exposure",
|
||||
"category": "sensitive-data-exposure",
|
||||
"title": "Potential token exposed",
|
||||
"severity": "critical",
|
||||
"evidence": {
|
||||
"url": "https://example.test/api/users",
|
||||
"value_preview": "ghp_…7890",
|
||||
},
|
||||
"standards": {
|
||||
"CWE": [{"id": "CWE-798", "url": "https://cwe.mitre.org/data/definitions/798.html"}],
|
||||
"CVE": [{"id": "CVE-2026-12345", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-12345"}],
|
||||
},
|
||||
}
|
||||
(root / "findings.jsonl").write_text(json.dumps(finding) + "\n", encoding="utf-8")
|
||||
modules = root / "modules"
|
||||
modules.mkdir()
|
||||
(modules / "example__sensitive_exposure.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"module": "sensitive_exposure",
|
||||
"target": "https://example.test",
|
||||
"status": "completed",
|
||||
"findings": [finding],
|
||||
"observations": {"responses_scanned": 2},
|
||||
"artifacts": [],
|
||||
"errors": [],
|
||||
"duration_seconds": 0.1,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return root
|
||||
|
||||
def test_normalizes_run_into_typed_graph(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
graph = build_visual_graph(self.make_run(directory))
|
||||
types = {node["type"] for node in graph["nodes"]}
|
||||
self.assertTrue({"run", "target", "endpoint", "parameter", "document", "module", "exposure", "standard", "cve"} <= types)
|
||||
relations = {edge["type"] for edge in graph["edges"]}
|
||||
self.assertTrue({"contains", "serves", "accepts", "executed", "reported", "exposes", "maps_to"} <= relations)
|
||||
self.assertEqual(graph["metadata"]["severity_counts"]["critical"], 1)
|
||||
|
||||
def test_loopback_server_serves_ui_and_graph(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
server = VisualServer(self.make_run(directory), 0)
|
||||
thread = threading.Thread(target=server.httpd.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
with urlopen(server.url, timeout=2) as response:
|
||||
self.assertIn(b"Visual investigation console", response.read())
|
||||
self.assertIn("default-src 'self'", response.headers["Content-Security-Policy"])
|
||||
with urlopen(server.url + "api/graph", timeout=2) as response:
|
||||
graph = json.loads(response.read())
|
||||
self.assertEqual(graph["metadata"]["run_id"], "run_visual")
|
||||
finally:
|
||||
server.httpd.shutdown()
|
||||
server.httpd.server_close()
|
||||
thread.join(timeout=2)
|
||||
|
||||
def test_rejects_non_run_directory(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
with self.assertRaises(ValueError):
|
||||
build_visual_graph(directory)
|
||||
|
||||
def test_accepts_local_file_url_for_run_directory(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
run = self.make_run(directory)
|
||||
self.assertEqual(resolve_run_directory(run.as_uri()), run.resolve())
|
||||
self.assertEqual(build_visual_graph(run.as_uri())["metadata"]["run_id"], "run_visual")
|
||||
|
||||
def test_visual_assets_keep_node_clicks_on_the_node(self):
|
||||
app = files("redflare.web").joinpath("app.js").read_text(encoding="utf-8")
|
||||
styles = files("redflare.web").joinpath("styles.css").read_text(encoding="utf-8")
|
||||
self.assertIn("group.setPointerCapture(event.pointerId)", app)
|
||||
self.assertIn("selectNode(node.id)", app)
|
||||
self.assertIn("centerOnNode(node)", app)
|
||||
self.assertIn('.graph-stage.dense .node[data-type="endpoint"] .node-label', styles)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,48 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from redflare.core.models import Target
|
||||
from redflare.core.standards import enrich_finding
|
||||
from redflare.modules.base import ModuleContext
|
||||
from redflare.modules.http import HTTPResponse
|
||||
from redflare.modules.vulnerabilities import CVEIntelligenceModule, fingerprint_response
|
||||
|
||||
|
||||
class CVEIntelligenceTests(unittest.TestCase):
|
||||
def test_fingerprints_only_explicit_versions(self):
|
||||
body = b'<meta name="generator" content="WordPress 6.4.2"><script src="jquery-3.6.0.min.js"></script>'
|
||||
values = fingerprint_response({"server": "nginx/1.24.0", "x-powered-by": "Express"}, body)
|
||||
products = {(item.product, item.version) for item in values}
|
||||
self.assertEqual(products, {("nginx", "1.24.0"), ("WordPress", "6.4.2"), ("jQuery", "3.6.0")})
|
||||
self.assertNotIn("Express", {item.product for item in values})
|
||||
|
||||
@patch("redflare.modules.vulnerabilities.query_nvd")
|
||||
@patch("redflare.modules.vulnerabilities.request")
|
||||
def test_emits_cve_findings_with_clickable_reference(self, get, nvd):
|
||||
get.return_value = HTTPResponse("https://example.test", 200, {"server": "nginx/1.24.0"}, b"")
|
||||
nvd.return_value = [{
|
||||
"id": "CVE-2026-12345",
|
||||
"published": "2026-01-02T00:00:00Z",
|
||||
"lastModified": "2026-01-03T00:00:00Z",
|
||||
"vulnStatus": "Analyzed",
|
||||
"descriptions": [{"lang": "en", "value": "Fixture vulnerability."}],
|
||||
"metrics": {"cvssMetricV31": [{"cvssData": {"baseScore": 9.8, "baseSeverity": "CRITICAL", "vectorString": "CVSS:3.1/AV:N"}}]},
|
||||
"references": [{"url": "https://vendor.example/advisory"}],
|
||||
}]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
context = ModuleContext("run_test", Path(directory), timeout=1)
|
||||
result = CVEIntelligenceModule().run(Target("https://example.test", "example.test", "https", 443), context)
|
||||
self.assertEqual(len(result.findings), 1)
|
||||
finding = enrich_finding(result.findings[0])
|
||||
self.assertEqual(finding.test_id, "RFV2-COMP-001")
|
||||
self.assertEqual(finding.severity, "critical")
|
||||
self.assertEqual(finding.standards["CVE"][0]["id"], "CVE-2026-12345")
|
||||
self.assertTrue(finding.standards["CVE"][0]["url"].startswith("https://nvd.nist.gov/"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user