mirror of
https://github.com/laramies/theHarvester.git
synced 2026-08-25 15:22:24 +02:00
* Model active evidence in result persistence * Expose active action diagnostics * Persist truthful DNS action evidence * Persist direct action evidence * feat: add authenticated durable run API v1 * Make API file interchange JSONL-only * Harden API evidence boundaries * Remove API rate limiter * Unify API runs with result persistence * Support target action runs * Keep DNS resolver selection action-neutral * Add action-neutral CLI resolver selection * Preserve API evidence across JSONL round trips * Complete HarvestView API run parity * Harden HarvestView run API contracts * Remove obsolete bundled network snapshots Delete the unused bundled AWS IP-range and resolver snapshots while preserving operator-supplied resolver file input. * refactor: canonicalize URL results * docs: define a bounded test budget * Standardize hostname and IP result names * fix(api): avoid duplicate evidence conflicts
111 lines
4.0 KiB
Python
111 lines
4.0 KiB
Python
from datetime import UTC, datetime
|
|
|
|
import pytest
|
|
|
|
from theHarvester.lib.active_evidence import ActionExecution, ActionObservation, ActiveEvidence, ArtifactReference
|
|
|
|
|
|
def screenshot_artifact() -> ArtifactReference:
|
|
return ArtifactReference(
|
|
kind='screenshot',
|
|
subject_kind='hostname',
|
|
subject_value='api.example.com',
|
|
path='screenshots/api.example.com.png',
|
|
media_type='image/png',
|
|
size_bytes=3,
|
|
sha256='0' * 64,
|
|
created_at=datetime(2026, 8, 9, 12, 0, tzinfo=UTC),
|
|
)
|
|
|
|
|
|
def test_active_evidence_owns_action_results_and_artifacts() -> None:
|
|
evidence = ActiveEvidence(
|
|
executions=(
|
|
ActionExecution.finish(
|
|
action='dns-resolve',
|
|
status='completed',
|
|
duration_ms=12.5,
|
|
groups={'ip': ['192.0.2.10', '192.0.2.10']},
|
|
),
|
|
ActionExecution.finish(
|
|
action='screenshot',
|
|
status='completed',
|
|
duration_ms=4.0,
|
|
groups={},
|
|
artifacts=(screenshot_artifact(),),
|
|
),
|
|
ActionExecution.finish(action='takeover', status='completed', duration_ms=2.0, groups={}),
|
|
)
|
|
)
|
|
|
|
assert evidence.executions[0].result_count == 1
|
|
assert evidence.executions[0].observations == (ActionObservation('ip', '192.0.2.10'),)
|
|
assert evidence.executions[1].result_count == 0
|
|
assert evidence.executions[1].artifacts == (screenshot_artifact(),)
|
|
|
|
|
|
def test_active_evidence_rejects_duplicate_actions() -> None:
|
|
with pytest.raises(ValueError, match='action executions must be unique'):
|
|
ActiveEvidence(
|
|
executions=(
|
|
ActionExecution('dns-resolve', 'completed', 1.0),
|
|
ActionExecution('dns-resolve', 'failed', 2.0, error_type='RuntimeError'),
|
|
)
|
|
)
|
|
|
|
|
|
def test_action_execution_rejects_noncanonical_observations_and_artifacts() -> None:
|
|
observation = ActionObservation('hostname', 'api.example.com')
|
|
artifact = screenshot_artifact()
|
|
|
|
with pytest.raises(ValueError, match='action observations must be deduplicated and sorted'):
|
|
ActionExecution('dns-brute', 'completed', 1.0, observations=(observation, observation))
|
|
with pytest.raises(ValueError, match='artifacts must be deduplicated and sorted'):
|
|
ActionExecution('screenshot', 'completed', 1.0, artifacts=(artifact, artifact))
|
|
|
|
|
|
def test_active_evidence_rejects_screenshot_results() -> None:
|
|
with pytest.raises(ValueError, match='screenshots must be stored as artifacts'):
|
|
ActionExecution.finish(
|
|
action='screenshot',
|
|
status='completed',
|
|
duration_ms=1.0,
|
|
groups={'screenshot': ['https://api.example.com']},
|
|
)
|
|
with pytest.raises(ValueError, match='known non-empty result'):
|
|
ArtifactReference(
|
|
kind='screenshot',
|
|
subject_kind='screenshot',
|
|
subject_value='https://api.example.com',
|
|
path='screenshots/api.example.com.png',
|
|
media_type='image/png',
|
|
size_bytes=3,
|
|
sha256='0' * 64,
|
|
created_at=datetime(2026, 8, 9, 12, 0, tzinfo=UTC),
|
|
)
|
|
|
|
|
|
def test_artifact_reference_rejects_invalid_metadata() -> None:
|
|
with pytest.raises(ValueError, match='sha256'):
|
|
ArtifactReference(
|
|
kind='screenshot',
|
|
subject_kind='hostname',
|
|
subject_value='api.example.com',
|
|
path='screenshots/api.example.com.png',
|
|
media_type='image/png',
|
|
size_bytes=3,
|
|
sha256='not-a-hash',
|
|
created_at=datetime(2026, 8, 9, 12, 0, tzinfo=UTC),
|
|
)
|
|
with pytest.raises(ValueError, match='timezone-aware'):
|
|
ArtifactReference(
|
|
kind='screenshot',
|
|
subject_kind='hostname',
|
|
subject_value='api.example.com',
|
|
path='screenshots/api.example.com.png',
|
|
media_type='image/png',
|
|
size_bytes=3,
|
|
sha256='0' * 64,
|
|
created_at=datetime(2026, 8, 9, 12, 0),
|
|
)
|