mirror of
https://github.com/nox-project/nox-framework.git
synced 2026-06-08 16:07:17 +00:00
NOX Framework v1.0.0
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""tests/__init__.py"""
|
||||
@@ -0,0 +1,26 @@
|
||||
"""tests/test_cracker.py — Unit tests for hash detection."""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from sources.helpers.cracker import detect_hash
|
||||
|
||||
|
||||
def test_md5():
|
||||
assert detect_hash("5f4dcc3b5aa765d61d8327deb882cf99") == "md5"
|
||||
|
||||
def test_sha1():
|
||||
assert detect_hash("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d") == "sha1"
|
||||
|
||||
def test_sha256():
|
||||
assert detect_hash("5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8") == "sha256"
|
||||
|
||||
def test_bcrypt():
|
||||
assert detect_hash("$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW") == "bcrypt"
|
||||
|
||||
def test_non_hash():
|
||||
assert detect_hash("notahash") is None
|
||||
assert detect_hash("") is None
|
||||
assert detect_hash("hello@world.com") is None
|
||||
|
||||
def test_uppercase_md5():
|
||||
assert detect_hash("5F4DCC3B5AA765D61D8327DEB882CF99") == "md5"
|
||||
@@ -0,0 +1,28 @@
|
||||
"""tests/test_detect.py — Unit tests for input type detection."""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from nox import Detect
|
||||
|
||||
|
||||
def test_email():
|
||||
assert Detect.qtype("user@example.com") == "email"
|
||||
assert Detect.qtype("first.last+tag@sub.domain.org") == "email"
|
||||
|
||||
def test_domain():
|
||||
assert Detect.qtype("example.com") == "domain"
|
||||
assert Detect.qtype("sub.example.co.uk") == "domain"
|
||||
|
||||
def test_ip():
|
||||
assert Detect.qtype("192.168.1.1") == "ip"
|
||||
assert Detect.qtype("8.8.8.8") == "ip"
|
||||
|
||||
def test_hash_md5():
|
||||
assert Detect.qtype("5f4dcc3b5aa765d61d8327deb882cf99") == "hash"
|
||||
|
||||
def test_hash_sha256():
|
||||
assert Detect.qtype("5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8") == "hash"
|
||||
|
||||
def test_username():
|
||||
assert Detect.qtype("johndoe") == "username"
|
||||
assert Detect.qtype("john_doe_99") == "username"
|
||||
@@ -0,0 +1,45 @@
|
||||
"""tests/test_identity.py — Unit tests for IdentityResolver Union-Find clustering."""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from nox import Record, IdentityResolver
|
||||
|
||||
|
||||
def _rec(email="", username="", password="", source="S"):
|
||||
return Record(source=source, email=email, username=username, password=password)
|
||||
|
||||
|
||||
def test_single_record_one_cluster():
|
||||
records = [_rec(email="a@b.com")]
|
||||
profiles = IdentityResolver(records).resolve()
|
||||
assert len(profiles) == 1
|
||||
|
||||
|
||||
def test_shared_password_merges_clusters():
|
||||
# password must be > 6 chars to be used as a pivot key
|
||||
records = [
|
||||
_rec(email="a@b.com", password="shared_password_long"),
|
||||
_rec(email="c@d.com", password="shared_password_long"),
|
||||
]
|
||||
profiles = IdentityResolver(records).resolve()
|
||||
assert len(profiles) == 1
|
||||
|
||||
|
||||
def test_distinct_records_separate_clusters():
|
||||
records = [
|
||||
_rec(email="a@b.com", password="uniquepassword1"),
|
||||
_rec(email="c@d.com", password="uniquepassword2"),
|
||||
]
|
||||
profiles = IdentityResolver(records).resolve()
|
||||
assert len(profiles) == 2
|
||||
|
||||
|
||||
def test_empty_records():
|
||||
profiles = IdentityResolver([]).resolve()
|
||||
assert profiles == []
|
||||
|
||||
|
||||
def test_hvt_flag_propagates():
|
||||
records = [_rec(email="admin@corp.com", password="secretpass")]
|
||||
profiles = IdentityResolver(records).resolve()
|
||||
assert profiles[0].is_hvt is True
|
||||
@@ -0,0 +1,39 @@
|
||||
"""tests/test_reporting.py — Unit tests for build_exec_summary."""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from sources.helpers.reporting import build_exec_summary
|
||||
|
||||
|
||||
def test_empty_records():
|
||||
summary = build_exec_summary({"records": [], "analysis": {}, "scan_meta": {}})
|
||||
assert summary["total_records"] == 0
|
||||
assert summary["cleartext_passwords"] == 0
|
||||
assert summary["nodes_discovered"] == 0
|
||||
|
||||
|
||||
def test_counts_cleartext():
|
||||
class R:
|
||||
email = "a@b.com"; username = ""; password = "secret"; risk_score = 50.0; is_hvt = False
|
||||
summary = build_exec_summary({"records": [R()], "analysis": {}, "scan_meta": {}})
|
||||
assert summary["cleartext_passwords"] == 1
|
||||
assert summary["total_records"] == 1
|
||||
|
||||
|
||||
def test_hvt_count():
|
||||
class R:
|
||||
email = "admin@corp.com"; username = ""; password = ""; risk_score = 80.0; is_hvt = True
|
||||
summary = build_exec_summary({"records": [R()], "analysis": {}, "scan_meta": {}})
|
||||
assert summary["hvt_count"] >= 1
|
||||
|
||||
|
||||
def test_bucket_critical():
|
||||
class R:
|
||||
email = "x@y.com"; username = ""; password = "pw"; risk_score = 95.0; is_hvt = False
|
||||
summary = build_exec_summary({"records": [R()], "analysis": {}, "scan_meta": {}})
|
||||
assert summary["buckets"]["Critical"] == 1
|
||||
|
||||
|
||||
def test_elapsed_formatting():
|
||||
summary = build_exec_summary({"records": [], "analysis": {}, "scan_meta": {"elapsed_seconds": 12.5}})
|
||||
assert summary["elapsed"] == "12.5s"
|
||||
@@ -0,0 +1,38 @@
|
||||
"""tests/test_risk.py — Unit tests for RiskEngine boundary values."""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from nox import Record, RiskEngine, Severity
|
||||
|
||||
|
||||
def _make(password="", breach_date="", source="TestSource", email="test@example.com"):
|
||||
r = Record(source=source, email=email, password=password, breach_date=breach_date)
|
||||
return RiskEngine.score(r)
|
||||
|
||||
|
||||
def test_score_returns_float():
|
||||
r = _make(password="hunter2")
|
||||
assert isinstance(r.risk_score, float)
|
||||
|
||||
|
||||
def test_score_in_range():
|
||||
r = _make(password="hunter2")
|
||||
assert 0.0 <= r.risk_score <= 100.0
|
||||
|
||||
|
||||
def test_no_password_lower_score():
|
||||
with_pw = _make(password="secret123")
|
||||
without_pw = _make(password="")
|
||||
assert with_pw.risk_score >= without_pw.risk_score
|
||||
|
||||
|
||||
def test_cleartext_password_raises_severity():
|
||||
r = _make(password="P@ssw0rd!")
|
||||
assert r.severity in (Severity.HIGH, Severity.CRITICAL, Severity.MEDIUM)
|
||||
|
||||
|
||||
def test_persistence_does_not_crash():
|
||||
records = [_make(password="reused", email="a@b.com"),
|
||||
_make(password="reused", email="a@b.com")]
|
||||
result = RiskEngine.apply_persistence(records)
|
||||
assert len(result) == 2
|
||||
@@ -0,0 +1,90 @@
|
||||
"""tests/test_scanner.py — Unit tests for AvalancheScanner dedup and depth cap."""
|
||||
import asyncio
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from sources.helpers.scanner import AvalancheScanner, _extract_ids_from_text as _extract_new_ids, _ids_from_records
|
||||
|
||||
|
||||
# ── _extract_new_ids ──────────────────────────────────────────────────
|
||||
|
||||
def test_extract_email():
|
||||
ids = _extract_new_ids("contact user@example.com for info")
|
||||
assert ("user@example.com", "email") in ids
|
||||
|
||||
|
||||
def test_extract_username_from_github():
|
||||
ids = _extract_new_ids("see github.com/johndoe for code")
|
||||
assert ("johndoe", "username") in ids
|
||||
|
||||
|
||||
def test_extract_no_false_positives():
|
||||
ids = _extract_new_ids("no identifiers here at all")
|
||||
assert ids == []
|
||||
|
||||
|
||||
# ── seen_assets dedup ─────────────────────────────────────────────────
|
||||
|
||||
class _FakeOrchestrator:
|
||||
"""Minimal orchestrator stub — records how many times each asset is scanned."""
|
||||
def __init__(self):
|
||||
self.scan_calls = []
|
||||
self.dorking_engine = _FakeDorkingEngine()
|
||||
|
||||
async def _full_async_scan(self, asset, qtype):
|
||||
self.scan_calls.append(asset)
|
||||
return []
|
||||
|
||||
def dork(self, asset, query_type=None):
|
||||
return []
|
||||
|
||||
def scrape(self, asset, query_type=None):
|
||||
return {"pastes": [], "credentials": [], "hashes": [], "telegram": [], "dork_misconfigs": []}
|
||||
|
||||
|
||||
class _FakeDorkingEngine:
|
||||
async def async_search(self, session, asset, qtype):
|
||||
return []
|
||||
|
||||
|
||||
def test_seen_assets_prevents_duplicate_scan():
|
||||
orc = _FakeOrchestrator()
|
||||
scanner = AvalancheScanner(orc)
|
||||
|
||||
async def _run():
|
||||
scanner.seen_assets.add("target@example.com")
|
||||
await asyncio.gather(
|
||||
scanner._process("target@example.com", depth=0, parent=None, found_in="seed"),
|
||||
scanner._process("target@example.com", depth=0, parent=None, found_in="seed"),
|
||||
)
|
||||
|
||||
asyncio.run(_run())
|
||||
# Should only have been scanned once (or zero times since it was pre-added to seen_assets)
|
||||
assert orc.scan_calls.count("target@example.com") <= 1
|
||||
|
||||
|
||||
def test_depth_cap_respected():
|
||||
orc = _FakeOrchestrator()
|
||||
scanner = AvalancheScanner(orc)
|
||||
|
||||
async def _run():
|
||||
await scanner._process("deep@example.com", depth=99, parent=None, found_in="seed")
|
||||
|
||||
asyncio.run(_run())
|
||||
assert "deep@example.com" not in orc.scan_calls
|
||||
|
||||
|
||||
def test_global_dork_url_dedup():
|
||||
orc = _FakeOrchestrator()
|
||||
scanner = AvalancheScanner(orc)
|
||||
scanner._seen_dork_urls.add("https://example.com/leak")
|
||||
|
||||
# Simulate accumulating a hit with a URL already seen
|
||||
hit = {"url": "https://example.com/leak", "title": "Leak", "snippet": ""}
|
||||
initial_len = len(scanner._dork_hits)
|
||||
url = hit.get("url", "")
|
||||
if url and url not in scanner._seen_dork_urls:
|
||||
scanner._seen_dork_urls.add(url)
|
||||
scanner._dork_hits.append(hit)
|
||||
|
||||
assert len(scanner._dork_hits) == initial_len # not added — already seen
|
||||
Reference in New Issue
Block a user