Files
NotoriousRebel ea5db87915 Merge master into dev to resolve PR #2607 conflicts
Preserve provider outcome release notes alongside the HTTP proxy wording. Keep the dev domain glossary and carry the master HTTP-only proxy policy into its relocated architecture guide. Retain RocketReach cancellation coverage while removing the obsolete global dependency stub.

Align provider lifecycle regressions with the master session-construction boundary: construction ValueError is normalized to transport-error, while cancellation, lifecycle RuntimeError, and teardown ValueError still propagate.
2026-09-09 21:41:46 -04:00

2173 lines
93 KiB
Python

from __future__ import annotations
import json
import sqlite3
from collections import Counter
from pathlib import Path
import pytest
from playwright.sync_api import Page, Route, expect
pytestmark = pytest.mark.harvestview_e2e
@pytest.mark.parametrize(
('route_pattern', 'response_path', 'status_code', 'detail', 'console_error'),
[
(
'**/api/v1/runs*',
'/api/v1/runs',
401,
'THEHARVESTER_API_KEY is not configured',
'Failed to load resource: the server responded with a status of 401 (Unauthorized)',
),
(
'**/api/v1/sources',
'/api/v1/sources',
503,
'Source catalog unavailable',
'Failed to load resource: the server responded with a status of 503 (Service Unavailable)',
),
(
'**/api/v1/schedules/health',
'/api/v1/schedules/health',
503,
'Scheduler unavailable',
'Failed to load resource: the server responded with a status of 503 (Service Unavailable)',
),
],
)
def test_schedule_page_fails_closed_when_prerequisites_are_unavailable(
harvestview_server_url: str,
page: Page,
browser_failures,
route_pattern: str,
response_path: str,
status_code: int,
detail: str,
console_error: str,
) -> None:
browser_failures.allow_response('GET', status_code, response_path)
browser_failures.allow_console_error(console_error)
page.route(route_pattern, lambda route: route.fulfill(status=status_code, json={'detail': detail}))
page.goto(f'{harvestview_server_url}/schedules')
expect(page.locator('#runtime-health')).to_have_text('Schedules unavailable')
expect(page.locator('#source-readiness')).to_have_text('Schedule prerequisites unavailable.')
expect(page.locator('#schedule-loading')).to_contain_text('Schedule prerequisites are unavailable.')
expect(page.locator('#create-schedule-button')).to_be_disabled()
expect(page.locator('#refresh-button')).to_be_enabled()
assert 'THEHARVESTER_API_KEY' not in page.locator('body').inner_text()
def test_schedule_page_recovers_without_losing_entered_values_or_submitting_while_unavailable(
harvestview_server_url: str,
page: Page,
browser_failures,
) -> None:
page.goto(f'{harvestview_server_url}/schedules')
page.locator('#schedule-name').fill('Existing schedule')
page.locator('#schedule-targets').fill('example.org')
page.locator('.source-option').filter(has_text='crtsh').locator('input').check()
page.locator('#create-schedule-button').click()
card = page.locator('.schedule-card').filter(has_text='Existing schedule')
expect(card).to_be_visible()
page.locator('#schedule-name').fill('Preserved schedule')
page.locator('#schedule-targets').fill('example.test')
crtsh = page.locator('.source-option').filter(has_text='crtsh').locator('input')
crtsh.check()
mutation_requests: list[str] = []
def guard_mutations(route: Route) -> None:
if route.request.method != 'GET':
mutation_requests.append(f'{route.request.method} {route.request.url}')
route.fulfill(status=409, json={'detail': 'Mutation blocked by test'})
return
route.fallback()
def fail_catalog(route: Route) -> None:
route.fulfill(status=503, json={'detail': 'Source catalog unavailable'})
page.route('**/api/v1/**', guard_mutations)
page.route('**/api/v1/sources', fail_catalog)
browser_failures.allow_response('GET', 503, '/api/v1/sources')
browser_failures.allow_console_error(
'Failed to load resource: the server responded with a status of 503 (Service Unavailable)'
)
page.locator('#refresh-button').click()
expect(page.locator('#create-schedule-button')).to_be_disabled()
for action in ('Edit', 'History', 'Run now', 'Pause', 'Delete'):
expect(card.get_by_role('button', name=action)).to_be_disabled()
page.evaluate(
"document.querySelector('#schedule-form').dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}))"
)
page.wait_for_timeout(100)
assert mutation_requests == []
page.unroute('**/api/v1/sources', fail_catalog)
page.locator('#refresh-button').click()
expect(page.locator('#runtime-health')).to_have_text('Preview mode · execution disabled')
expect(page.locator('#create-schedule-button')).to_be_enabled()
expect(page.locator('#schedule-name')).to_have_value('Preserved schedule')
expect(page.locator('#schedule-targets')).to_have_value('example.test')
expect(page.locator('.source-option').filter(has_text='crtsh').locator('input')).to_be_checked()
def test_schedule_page_creates_and_manages_two_target_passive_schedule(
harvestview_server_url: str,
page: Page,
) -> None:
page.set_viewport_size({'width': 390, 'height': 844})
page.goto(f'{harvestview_server_url}/schedules')
expect(page.get_by_role('heading', name='Create a schedule')).to_be_visible()
expect(page.locator('#runtime-health')).to_have_text('Preview mode · execution disabled')
expect(page.locator('#source-readiness')).to_contain_text('passive sources are ready')
expect(page.locator('.source-option.is-unready').first).to_contain_text('Credentials required:')
theme_button = page.locator('#theme-button')
for theme in ('light', 'dark', 'system'):
theme_button.click()
expect(page.locator('html')).to_have_attribute('data-theme', theme)
assert page.evaluate("localStorage.getItem('runs-theme')") == 'system'
page.locator('#schedule-name').focus()
page.keyboard.press('Tab')
expect(page.locator('#schedule-targets')).to_be_focused()
page.locator('#schedule-name').fill('Blog inventory')
page.locator('#schedule-targets').fill('Example.Test.\nexample.test')
expect(page.locator('#target-summary')).to_have_text('1 unique target')
page.locator('#schedule-targets').fill('example.test\nexample.org')
expect(page.locator('#target-summary')).to_have_text('2 unique targets')
page.locator('#schedule-frequency').select_option('monthly')
page.locator('#schedule-start').fill('2030-01-31T09:00')
page.locator('#schedule-timezone').fill('America/New_York')
expect(page.locator('#monthly-help')).to_be_visible()
expect(page.locator('#monthly-help')).to_contain_text('final day')
crtsh = page.locator('.source-option').filter(has_text='crtsh')
expect(crtsh.locator('input')).to_be_enabled()
crtsh.locator('input').check()
page.locator('#create-schedule-button').click()
card = page.locator('.schedule-card').filter(has_text='Blog inventory')
expect(card).to_be_visible()
expect(card).to_contain_text('example.test, example.org')
expect(card).to_contain_text('2')
expect(card).to_contain_text('Monthly')
card.get_by_text('Upcoming occurrences').click()
expect(card.locator('.upcoming-occurrences')).to_contain_text('Feb 28, 2030')
expect(card.get_by_role('button', name='Pause')).to_be_visible()
schedules = page.evaluate("fetch('/api/v1/schedules').then((response) => response.json())")
assert schedules[0]['run']['dns_resolve'] is False
assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth')
card.get_by_role('button', name='Edit').click()
expect(page.get_by_role('heading', name='Edit schedule')).to_be_visible()
expect(page.locator('#schedule-name')).to_have_value('Blog inventory')
expect(page.locator('#schedule-frequency')).to_have_value('monthly')
page.get_by_role('button', name='Cancel edit').click()
expect(page.locator('#schedule-name')).to_be_focused()
expect(page.get_by_role('heading', name='Create a schedule')).to_be_visible()
card.get_by_role('button', name='Edit').click()
card.get_by_role('button', name='Pause').click()
card = page.locator('.schedule-card').filter(has_text='Blog inventory')
expect(card.get_by_role('button', name='Resume')).to_be_visible()
page.locator('#schedule-name').fill('Blog inventory every week')
page.locator('#schedule-frequency').select_option('daily')
expect(page.locator('#monthly-help')).to_be_hidden()
page.locator('#schedule-interval').fill('7')
page.get_by_role('button', name='Save changes').click()
card = page.locator('.schedule-card').filter(has_text='Blog inventory every week')
expect(card).to_be_visible()
expect(card).to_contain_text('Every 7 days')
expect(card.get_by_role('button', name='Resume')).to_be_visible()
expect(page.get_by_role('heading', name='Create a schedule')).to_be_visible()
expect(page.get_by_role('button', name='Cancel edit')).to_be_hidden()
updated = page.evaluate("fetch('/api/v1/schedules').then((response) => response.json())")
assert updated[0]['timing']['frequency'] == 'daily'
assert updated[0]['timing']['interval'] == 7
card.get_by_role('button', name='Resume').click()
card = page.locator('.schedule-card').filter(has_text='Blog inventory every week')
expect(card.get_by_role('button', name='Pause')).to_be_visible()
card.get_by_role('button', name='Pause').click()
card = page.locator('.schedule-card').filter(has_text='Blog inventory')
expect(card.get_by_role('button', name='Resume')).to_be_visible()
card.get_by_role('button', name='Resume').click()
card = page.locator('.schedule-card').filter(has_text='Blog inventory')
expect(card.get_by_role('button', name='Pause')).to_be_visible()
page.route(
'**/api/v1/schedules/*/run-now',
lambda route: route.fulfill(
status=202,
json={
'schedule_id': 'browser-test',
'scheduled_for': '2026-08-20T12:00:00+00:00',
'run_ids': ['one', 'two'],
'skipped_targets': [],
'errors': [],
},
),
)
page.once('dialog', lambda dialog: dialog.accept())
card.get_by_role('button', name='Run now').click()
expect(page.locator('#toast')).to_have_text('Queued 2 run(s); skipped 0.')
page.route(
'**/api/v1/schedules/*/dispatches?limit=1000',
lambda route: route.fulfill(
json=[
{
'schedule_id': 'browser-test',
'scheduled_for': '2026-08-20T12:00:00+00:00',
'target': target,
'run_id': f'run-{index}',
'state': 'queued',
'error': None,
}
for index, target in enumerate(('example.test', 'example.org', 'example.net'), start=1)
]
),
)
history_button = card.get_by_role('button', name='History')
history_button.focus()
history_button.press('Enter')
expect(page.get_by_role('dialog')).to_be_visible()
expect(page.locator('#close-dispatch-dialog')).to_be_focused()
expect(page.locator('#dispatch-grid.tabulator')).to_be_visible()
expect(page.locator('#dispatch-grid')).to_contain_text('example.test')
expect(page.locator('#dispatch-grid')).to_contain_text('example.org')
expect(page.locator('#dispatch-grid')).to_contain_text('example.net')
page.keyboard.press('Escape')
expect(page.get_by_role('dialog')).to_be_hidden()
expect(history_button).to_be_focused()
page.once('dialog', lambda dialog: dialog.accept())
card.get_by_role('button', name='Delete').click()
expect(page.locator('#schedule-empty')).to_be_visible()
def test_schedule_page_rejects_a_non_loopback_host_in_browser(
harvestview_server_url: str,
page: Page,
browser_failures,
) -> None:
browser_failures.allow_console_error('Failed to load resource: the server responded with a status of 403 (Forbidden)')
remote_url = harvestview_server_url.replace('127.0.0.1', 'attacker.example') + '/schedules'
def forward_non_loopback_host(route: Route) -> None:
rejected = page.context.request.get(
f'{harvestview_server_url}/schedules',
headers={'Host': 'attacker.example'},
)
route.fulfill(status=rejected.status, headers=rejected.headers, body=rejected.body())
page.route(remote_url, forward_non_loopback_host)
response = page.goto(remote_url)
assert response is not None
assert response.status == 403
expect(page.locator('body')).to_contain_text('theHarvester is available only on localhost')
def test_schedule_clone_requires_fresh_authorization_for_active_work(
harvestview_server_url: str,
page: Page,
) -> None:
run = {
'run_id': 'active-template',
'target': 'source.example.test',
'status': 'completed',
'origin': 'local',
'created_at': '2026-08-19T12:00:00+00:00',
'started_at': '2026-08-19T12:00:00+00:00',
'completed_at': '2026-08-19T12:01:00+00:00',
'cancellation_requested_at': None,
'evidence_status': 'complete',
'result_count': 0,
'activities': ['P1'],
'sources': [],
'request': {
'target': 'source.example.test',
'sources': [],
'start': 25,
'dns_recursive_depth': 1,
},
'source_executions': [],
'action_executions': [],
'results': [],
'screenshots': [],
'log': '',
'error': None,
}
page.route(
f'{harvestview_server_url}/api/v1/runs?limit=500',
lambda route: route.fulfill(json=[run]),
)
page.route(
f'{harvestview_server_url}/api/v1/runs/active-template',
lambda route: route.fulfill(json=run),
)
page.goto(f'{harvestview_server_url}/schedules')
page.get_by_text('Clone all settings from an existing run', exact=True).click()
page.locator('#template-run').select_option('active-template')
expect(page.locator('#authorization-row')).to_be_visible()
page.locator('#schedule-name').fill('Active clone')
page.locator('#schedule-targets').fill('example.test')
page.locator('#create-schedule-button').click()
expect(page.locator('#form-error')).to_have_text(
'Could not create schedule: Confirm authorization for the cloned DNS or direct-interaction activity. '
'Review the schedule values and try again.'
)
page.locator('#authorization-confirmation').check()
page.locator('#create-schedule-button').click()
expect(page.locator('.schedule-card').filter(has_text='Active clone')).to_be_visible()
schedules = page.evaluate("fetch('/api/v1/schedules').then((response) => response.json())")
active_clone = next(schedule for schedule in schedules if schedule['name'] == 'Active clone')
assert active_clone['run']['start'] == 25
assert active_clone['run']['dns_recursive_depth'] == 1
card = page.locator('.schedule-card').filter(has_text='Active clone')
card.get_by_role('button', name='Edit').click()
expect(page.locator('#authorization-row')).to_be_visible()
expect(page.locator('#authorization-confirmation')).not_to_be_checked()
page.locator('#schedule-name').fill('Updated active clone')
page.get_by_role('button', name='Save changes').click()
expect(page.locator('#form-error')).to_have_text(
'Could not save schedule: Confirm authorization for the selected DNS or direct-interaction activity. '
'Review the schedule values and try again.'
)
page.locator('#authorization-confirmation').check()
page.get_by_role('button', name='Save changes').click()
expect(page.locator('.schedule-card').filter(has_text='Updated active clone')).to_be_visible()
updated_schedules = page.evaluate("fetch('/api/v1/schedules').then((response) => response.json())")
updated_clone = next(schedule for schedule in updated_schedules if schedule['name'] == 'Updated active clone')
assert updated_clone['run']['start'] == 25
assert updated_clone['run']['dns_recursive_depth'] == 1
def write_jsonl_evidence(path: Path, evidence: dict[str, object]) -> None:
results = evidence.get('results', [])
assert isinstance(results, list)
counts = Counter(str(result['type']) for result in results if isinstance(result, dict))
summary = {
'type': 'summary',
'run_id': evidence['run_id'],
'target': evidence['target'],
'started_at': evidence['started_at'],
'completed_at': evidence['completed_at'],
'evidence_status': evidence['status'],
'source_executions': evidence.get('source_executions', []),
'action_executions': evidence.get('action_executions', []),
'artifacts': evidence.get('artifacts', []),
'result_count': len(results),
'counts': dict(sorted(counts.items())),
}
records = [
summary,
*[
{**result, 'sources': result.get('sources', []), 'actions': result.get('actions', [])}
for result in results
if isinstance(result, dict)
],
]
path.write_text(''.join(json.dumps(record, sort_keys=True) + '\n' for record in records), encoding='utf-8')
def record_api_statuses(page: Page, server_url: str) -> dict[str, int]:
api_statuses: dict[str, int] = {}
def record_response(response) -> None:
if response.url.startswith(f'{server_url}/api/v1/'):
api_statuses[response.url.removeprefix(server_url)] = response.status
page.on('response', record_response)
return api_statuses
def test_fresh_browser_session_authenticates_through_harvestview(
harvestview_server_url: str,
page: Page,
) -> None:
api_statuses = record_api_statuses(page, harvestview_server_url)
page.goto(f'{harvestview_server_url}/')
expect(page.get_by_role('heading', name='No enumeration runs yet')).to_be_visible()
assert api_statuses['/api/v1/sources'] == 200
assert api_statuses['/api/v1/runs'] == 200
expect(page.get_by_text('Invalid API key')).to_have_count(0)
def test_browser_session_stays_authenticated_after_server_restart(
harvestview_server,
page: Page,
) -> None:
page.goto(f'{harvestview_server.url}/')
expect(page.get_by_role('heading', name='No enumeration runs yet')).to_be_visible()
harvestview_server.restart()
page.reload()
expect(page.get_by_role('heading', name='No enumeration runs yet')).to_be_visible()
expect(page.get_by_text('Invalid API key')).to_have_count(0)
@pytest.mark.parametrize('viewport', [{'width': 1440, 'height': 900}, {'width': 390, 'height': 844}])
def test_versioned_assets_and_tooltips_work_at_supported_viewports(
harvestview_server_url: str,
page: Page,
viewport: dict[str, int],
) -> None:
page.set_viewport_size(viewport)
page.goto(f'{harvestview_server_url}/')
stylesheet = page.locator('link[href*="/static/harvestview/app.css"]')
script = page.locator('script[src*="/static/harvestview/app.js"]')
assert '?v=' in (stylesheet.get_attribute('href') or '')
assert '?v=' in (script.get_attribute('src') or '')
assert '{{' not in (stylesheet.get_attribute('href') or '')
assert '{{' not in (script.get_attribute('src') or '')
page.get_by_role('button', name='Start enumeration').first.click()
tooltip = page.get_by_role('button', name='Explain discovery sources')
tooltip.hover()
page.wait_for_function(
"node => getComputedStyle(node, '::after').opacity === '1'",
arg=tooltip.element_handle(),
)
tooltip_content = tooltip.evaluate("node => getComputedStyle(node, '::after').content")
assert 'Credential warnings identify sources that cannot start' in tooltip_content
assert 'Credential warnings identify sources that cannot start' in tooltip.get_attribute('aria-description')
assert tooltip.bounding_box()['width'] >= 44
assert tooltip.bounding_box()['height'] >= 44
page.locator('#submit-run-button').scroll_into_view_if_needed()
assert page.locator('#submit-run-button').evaluate(
"""button => {
const buttonBox = button.getBoundingClientRect();
const dialogBox = button.closest('dialog').getBoundingClientRect();
return buttonBox.top >= dialogBox.top && buttonBox.bottom <= dialogBox.bottom && buttonBox.bottom <= innerHeight;
}"""
)
assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth')
def test_mobile_source_picker_avoids_nested_scroll_and_blocks_unconfigured_sources(
harvestview_server_url: str,
page: Page,
) -> None:
page.set_viewport_size({'width': 390, 'height': 844})
page.goto(f'{harvestview_server_url}/')
page.get_by_role('button', name='Start enumeration').first.click()
assert page.locator('#source-groups').evaluate(
'node => ({maxHeight: getComputedStyle(node).maxHeight, overflowY: getComputedStyle(node).overflowY})'
) == {'maxHeight': 'none', 'overflowY': 'visible'}
p0_group = page.locator('[data-activity="P0"]')
p0_group.locator('summary').click()
expect(p0_group).not_to_have_attribute('open', '')
expect(page.locator('[data-activity="P1"] summary')).to_be_visible()
censys = page.locator('.source-choice').filter(has_text='censys')
expect(censys.locator('input')).to_be_disabled()
expect(censys).to_contain_text('Needs configuration')
crtsh = page.locator('.source-choice').filter(has_text='crtsh')
expect(crtsh.locator('input')).to_be_enabled()
expect(crtsh).to_contain_text('Ready')
expect(page.locator('#source-selection-summary')).to_have_text('Selected 1 ready source: crtsh.')
expect(page.locator('#final-authorization-summary')).to_contain_text('Target not set')
expect(page.locator('#final-authorization-summary')).to_contain_text('1 source: crtsh')
page.locator('#run-target').fill('example.com')
page.locator('[name="screenshot"]').check()
expect(page.locator('#final-authorization-summary')).to_contain_text('Target example.com')
expect(page.locator('#final-authorization-summary')).to_contain_text('P2 selected')
p0_group.locator('summary').click()
crtsh.locator('input').uncheck()
expect(page.locator('#source-selection-summary')).to_have_text('Selected 0 ready sources.')
def test_imported_run_separates_original_execution_from_local_import(
harvestview_server_url: str,
page: Page,
) -> None:
run = {
'run_id': 'imported-run',
'target': 'example.test',
'status': 'completed',
'origin': 'imported',
'created_at': '2026-08-16T05:37:52+00:00',
'started_at': '2026-08-15T03:10:00+00:00',
'completed_at': '2026-08-15T03:10:08+00:00',
'cancellation_requested_at': None,
'evidence_status': 'complete',
'result_count': 0,
'activities': ['P0'],
'sources': ['crtsh'],
'request': {'sources': ['crtsh'], 'filename': 'evidence.jsonl'},
'source_executions': [],
'action_executions': [],
'results': [],
'screenshots': [],
'log': '',
'error': None,
}
page.route(f'{harvestview_server_url}/api/v1/runs', lambda route: route.fulfill(json=[run]))
page.route(f'{harvestview_server_url}/api/v1/runs/imported-run', lambda route: route.fulfill(json=run))
page.goto(f'{harvestview_server_url}/')
facts = page.locator('#run-facts')
expect(facts).to_contain_text('OriginImported evidence')
expect(facts).to_contain_text('Imported')
expect(facts).to_contain_text('Original started')
expect(facts).to_contain_text('Original completed')
expect(facts).not_to_contain_text('Submitted')
expect(page.locator('#lifecycle-track strong')).to_have_text(['Original started', 'Original completed', 'Imported'])
expect(page.locator('#lifecycle-note')).to_contain_text('original execution timing')
def test_hostname_comparison_filters_persisted_run_differences(
harvestview_server_url: str,
page: Page,
) -> None:
changes = [
{
'change_type': 'newly_reported',
'hostname': 'newly-reported.example.test',
'sources_in_previous_run': [],
'sources_in_current_run': ['beta'],
'reported_by_one_source': True,
'previous_resolution_evidence': 'not-checked',
'current_resolution_evidence': 'positive',
'previous_addressability': None,
'current_addressability': 'currently-addressable',
'incomplete_source_outcomes': [],
},
{
'change_type': 'no_longer_reported',
'hostname': 'no-longer-reported.example.test',
'sources_in_previous_run': ['alpha', 'beta'],
'sources_in_current_run': [],
'reported_by_one_source': False,
'previous_resolution_evidence': 'positive',
'current_resolution_evidence': 'not-checked',
'previous_addressability': 'currently-addressable',
'current_addressability': None,
'incomplete_source_outcomes': [],
},
{
'change_type': 'uncertain',
'hostname': 'uncertain.example.test',
'sources_in_previous_run': [],
'sources_in_current_run': ['beta'],
'reported_by_one_source': True,
'previous_resolution_evidence': 'not-checked',
'current_resolution_evidence': 'not-retained',
'previous_addressability': None,
'current_addressability': None,
'incomplete_source_outcomes': [
{'source': 'beta', 'status': 'partial', 'error_type': 'TimeoutError', 'stop_reason': 'timeout'}
],
},
{
'change_type': 'still_reported',
'hostname': 'stable.example.test',
'sources_in_previous_run': ['alpha'],
'sources_in_current_run': ['alpha'],
'reported_by_one_source': True,
'previous_resolution_evidence': 'positive',
'current_resolution_evidence': 'positive',
'previous_addressability': 'currently-addressable',
'current_addressability': 'currently-addressable',
'incomplete_source_outcomes': [],
},
]
run = {
'run_id': 'comparison-run',
'target': 'example.test',
'status': 'completed',
'origin': 'local',
'created_at': '2026-08-20T12:00:00+00:00',
'started_at': '2026-08-20T12:00:00+00:00',
'completed_at': '2026-08-20T12:01:00+00:00',
'cancellation_requested_at': None,
'evidence_status': 'complete',
'result_count': 0,
'activities': ['P0'],
'sources': ['alpha', 'beta'],
'request': {'target': 'example.test', 'sources': ['alpha', 'beta']},
'source_executions': [],
'action_executions': [],
'results': [],
'screenshots': [],
'log': '',
'error': None,
'hostname_comparison': {
'target': 'example.test',
'comparison_count': 1,
'comparisons': [
{
'run_id': 'comparison-run',
'completed_at': '2026-08-20T12:01:00+00:00',
'previous_comparable_run_id': 'previous-run',
'previous_comparable_run_completed_at': '2026-08-19T12:01:00+00:00',
'compared_sources': ['alpha', 'beta'],
'counts': {
'newly_reported': 1,
'still_reported': 1,
'no_longer_reported': 1,
'uncertain': 1,
},
}
],
'hostname_differences': changes,
},
}
page.route(f'{harvestview_server_url}/api/v1/runs', lambda route: route.fulfill(json=[run]))
page.route(f'{harvestview_server_url}/api/v1/runs/comparison-run', lambda route: route.fulfill(json=run))
page.goto(f'{harvestview_server_url}/')
panel = page.locator('#hostname-comparison-section')
rows = page.locator('#hostname-comparison-body tr')
expect(panel).to_be_visible()
expect(panel).to_contain_text('Uncertain means', ignore_case=True)
expect(rows).to_have_count(3)
expect(panel).to_contain_text('TimeoutError')
expect(panel).not_to_contain_text('stable.example.test')
page.locator('#comparison-change-filter').select_option('still_reported')
expect(page.locator('#comparison-still-reported-filter')).to_be_checked()
expect(rows).to_have_count(1)
expect(panel).to_contain_text('stable.example.test')
page.locator('#comparison-still-reported-filter').uncheck()
expect(page.locator('#comparison-change-filter')).to_have_value('')
expect(rows).to_have_count(3)
page.locator('#comparison-single-source-filter').check()
expect(rows).to_have_count(2)
expect(panel).not_to_contain_text('no-longer-reported.example.test')
page.locator('#comparison-single-source-filter').uncheck()
page.locator('#comparison-still-reported-filter').check()
expect(rows).to_have_count(4)
expect(panel).to_contain_text('stable.example.test')
page.locator('#comparison-source-filter').select_option('alpha')
expect(rows).to_have_count(2)
expect(panel).not_to_contain_text('newly-reported.example.test')
page.locator('#comparison-source-filter').select_option('')
page.locator('#comparison-resolution-filter').select_option('not-retained')
expect(rows).to_have_count(1)
expect(panel).to_contain_text('uncertain.example.test')
def test_disabled_worker_rejects_submission_without_creating_a_run(
harvestview_server_url: str,
page: Page,
browser_failures,
) -> None:
browser_failures.allow_response('POST', 503, '/api/v1/runs')
browser_failures.allow_console_error(
'Failed to load resource: the server responded with a status of 503 (Service Unavailable)'
)
page.goto(f'{harvestview_server_url}/')
page.get_by_role('button', name='Start enumeration').first.click()
page.locator('#run-target').fill('example.com')
with page.expect_response(
lambda response: response.url == f'{harvestview_server_url}/api/v1/runs' and response.request.method == 'POST'
) as submission:
page.locator('#submit-run-button').click()
assert submission.value.status == 503
expect(page.locator('#new-run-error')).to_have_text('theHarvester execution worker is disabled')
history = page.context.request.get(f'{harvestview_server_url}/api/v1/runs')
assert history.status == 200
assert history.json() == []
def test_harvestview_can_submit_overridable_execution_controls(
harvestview_server_url: str,
page: Page,
browser_failures,
tmp_path: Path,
) -> None:
browser_failures.allow_response('POST', 503, '/api/v1/runs')
browser_failures.allow_console_error(
'Failed to load resource: the server responded with a status of 503 (Service Unavailable)'
)
captured: dict[str, object] = {}
def capture_submission(route: Route) -> None:
if route.request.method != 'POST':
route.continue_()
return
captured.update(route.request.post_data_json)
route.fulfill(status=503, json={'detail': 'Controls captured'})
page.route(f'{harvestview_server_url}/api/v1/runs', capture_submission)
page.goto(f'{harvestview_server_url}/')
start_button = page.get_by_role('button', name='Start enumeration').first
expect(start_button).to_be_enabled()
page.set_default_timeout(2_000)
start_button.click()
page.get_by_role('button', name='Clear', exact=True).click()
page.locator('#run-target').fill('example.com')
page.get_by_text('Advanced execution controls', exact=True).click()
page.locator('#run-start').fill('25')
page.locator('#source-workers').fill('7')
page.locator('#run-deadline').fill('86400')
page.locator('[name="proxies"]').check()
page.locator('[name="shodan"]').check()
page.locator('[name="routeviews"]').check()
page.locator('[name="dns_lookup"]').check()
page.locator('[name="takeover"]').check()
page.locator('[name="api_scan"]').check()
page.locator('#api-scan-paths').fill('/api/v2\n/health')
page.locator('#dns-recursive-depth').fill('3')
page.locator('#dns-recursive-query-limit').fill('1234')
page.locator('#dns-recursive-runtime-seconds').fill('12.5')
resolver_file = tmp_path / 'resolvers.txt'
resolver_file.write_text('192.0.2.53\n198.51.100.53\n203.0.113.53\n', encoding='utf-8')
page.locator('#dns-resolver-file').set_input_files(resolver_file)
expect(page.locator('#dns-resolvers')).to_have_value('192.0.2.53,198.51.100.53,203.0.113.53')
expect(page.locator('#activity-summary')).to_have_text('P0 selected · P1 selected · P2 selected')
page.locator('[data-activity="P0"] input[value="crtsh"]').check()
page.locator('#submit-run-button').click()
expect(page.locator('#new-run-error')).to_have_text('Controls captured')
assert captured == {
'target': 'example.com',
'sources': ['crtsh'],
'limit': 500,
'start': 25,
'source_workers': 7,
'deadline_seconds': 86_400,
'proxies': True,
'no_hosts': False,
'dns_lookup': True,
'dns_resolve': False,
'dns_resolvers': ['192.0.2.53', '198.51.100.53', '203.0.113.53'],
'dns_recursive_depth': 3,
'dns_recursive_query_limit': 1_234,
'dns_recursive_runtime_seconds': 12.5,
'dns_brute': False,
'shodan': True,
'routeviews': True,
'screenshot': False,
'takeover': True,
'api_scan': True,
'api_scan_paths': ['/api/v2', '/health'],
'vhost': False,
'vhost_endpoint': '',
'vhost_candidates': [],
'vhost_request_limit': 100,
'vhost_runtime_seconds': 30,
'vhost_timeout_seconds': 5,
'vhost_concurrency': 5,
'vhost_insecure': False,
}
def test_harvestview_submits_hostname_exclusion(harvestview_server_url: str, page: Page, browser_failures) -> None:
browser_failures.allow_response('POST', 503, '/api/v1/runs')
browser_failures.allow_console_error(
'Failed to load resource: the server responded with a status of 503 (Service Unavailable)'
)
captured: dict[str, object] = {}
def capture_submission(route: Route) -> None:
if route.request.method != 'POST':
route.continue_()
return
captured.update(route.request.post_data_json)
route.fulfill(status=503, json={'detail': 'Hostname exclusion captured'})
page.route(f'{harvestview_server_url}/api/v1/runs', capture_submission)
page.goto(f'{harvestview_server_url}/')
page.get_by_role('button', name='Start enumeration').first.click()
page.get_by_role('button', name='Clear', exact=True).click()
page.locator('#run-target').fill('example.com')
page.get_by_text('Advanced execution controls', exact=True).click()
page.locator('[name="no_hosts"]').check()
page.locator('[data-activity="P0"] input[value="apis-guru"]').check()
page.locator('#submit-run-button').click()
expect(page.locator('#new-run-error')).to_have_text('Hostname exclusion captured')
assert captured['sources'] == ['apis-guru']
assert captured['no_hosts'] is True
def test_harvestview_requires_complete_inputs_for_a_vhost_only_run(
harvestview_server_url: str,
page: Page,
browser_failures,
) -> None:
browser_failures.allow_response('POST', 503, '/api/v1/runs')
browser_failures.allow_console_error(
'Failed to load resource: the server responded with a status of 503 (Service Unavailable)'
)
captured: dict[str, object] = {}
def capture_submission(route: Route) -> None:
if route.request.method != 'POST':
route.continue_()
return
captured.update(route.request.post_data_json)
route.fulfill(status=503, json={'detail': 'Virtual-host controls captured'})
page.route(f'{harvestview_server_url}/api/v1/runs', capture_submission)
page.goto(f'{harvestview_server_url}/')
page.get_by_role('button', name='Start enumeration').first.click()
page.get_by_role('button', name='Clear', exact=True).click()
page.locator('#run-target').fill('example.com')
page.get_by_text('P2 · Virtual-host discovery', exact=True).click()
page.locator('#vhost-endpoint').fill('https://192.0.2.10:443/')
expect(page.locator('#activity-summary')).to_have_text('P0 off · P1 off · P2 selected')
page.locator('#submit-run-button').click()
expect(page.locator('#new-run-error')).to_have_text(
'Virtual-host discovery without sources requires both a literal-IP endpoint and at least one candidate hostname.'
)
assert captured == {}
page.locator('#vhost-candidates').fill('admin.example.com\npreview.example.com')
page.locator('#submit-run-button').click()
expect(page.locator('#new-run-error')).to_have_text('Virtual-host controls captured')
assert captured['sources'] == []
assert captured['dns_resolvers']
assert captured['takeover'] is False
assert captured['api_scan_paths'] == []
assert captured['vhost'] is True
assert captured['vhost_endpoint'] == 'https://192.0.2.10:443/'
assert captured['vhost_candidates'] == ['admin.example.com', 'preview.example.com']
assert captured['vhost_request_limit'] == 100
assert captured['vhost_runtime_seconds'] == 30
assert captured['vhost_timeout_seconds'] == 5
assert captured['vhost_concurrency'] == 5
assert captured['vhost_insecure'] is False
def test_harvestview_submits_a_target_only_api_scan(
harvestview_server_url: str,
page: Page,
browser_failures,
) -> None:
browser_failures.allow_response('POST', 503, '/api/v1/runs')
browser_failures.allow_console_error(
'Failed to load resource: the server responded with a status of 503 (Service Unavailable)'
)
captured: dict[str, object] = {}
def capture_submission(route: Route) -> None:
if route.request.method != 'POST':
route.continue_()
return
captured.update(route.request.post_data_json)
route.fulfill(status=503, json={'detail': 'Target-only action captured'})
page.route(f'{harvestview_server_url}/api/v1/runs', capture_submission)
page.goto(f'{harvestview_server_url}/')
page.get_by_role('button', name='Start enumeration').first.click()
page.get_by_role('button', name='Clear', exact=True).click()
page.locator('#run-target').fill('api.example.test')
page.locator('[name="api_scan"]').check()
page.get_by_text('Advanced execution controls', exact=True).click()
page.locator('#api-scan-paths').fill('/api/v2\n/health')
page.locator('#submit-run-button').click()
expect(page.locator('#new-run-error')).to_have_text('Target-only action captured')
assert captured['sources'] == []
assert captured['api_scan'] is True
assert captured['api_scan_paths'] == ['/api/v2', '/health']
def test_harvestview_renders_grouped_virtual_host_observations(
harvestview_server_url: str,
page: Page,
) -> None:
run = {
'run_id': 'vhost-run',
'target': 'example.com',
'status': 'completed',
'origin': 'local',
'created_at': '2026-08-05T12:00:00+00:00',
'started_at': '2026-08-05T12:00:01+00:00',
'completed_at': '2026-08-05T12:00:05+00:00',
'cancellation_requested_at': None,
'evidence_status': 'complete',
'result_count': 2,
'activities': ['P0', 'P2'],
'sources': ['crtsh'],
'request': {
'sources': ['crtsh'],
'limit': 25,
'deadline_seconds': 300,
'vhost': True,
'vhost_endpoint': '',
'vhost_candidates': [],
'vhost_request_limit': 100,
'vhost_runtime_seconds': 30,
'vhost_timeout_seconds': 5,
'vhost_concurrency': 5,
'vhost_insecure': False,
},
'source_executions': [],
'action_executions': [
{'action': 'vhost', 'status': 'completed', 'result_count': 1, 'duration_ms': 125},
],
'results': [
{
'type': 'hostname',
'value': 'admin.example.com',
'sources': [],
'actions': ['vhost'],
'observations': [
{
'endpoint': 'https://192.0.2.10:443/',
'phase': 'body',
'status': 200,
'location': None,
'context_status': 200,
'control_status': 200,
'confirmation_body_sha256': 'a' * 64,
'tls_verified': True,
'distinct_signals': ['body_sha256'],
},
{
'endpoint': 'http://198.51.100.20:80/',
'phase': 'body',
'status': 302,
'location': '/login',
'context_status': 404,
'control_status': 404,
'confirmation_body_sha256': None,
'tls_verified': None,
'distinct_signals': ['status', 'location'],
},
],
},
{
'type': 'hostname',
'value': 'www.example.com',
'sources': ['crtsh'],
'actions': [],
},
],
'screenshots': [],
'log': '',
'error': None,
}
page.route(f'{harvestview_server_url}/api/v1/runs', lambda route: route.fulfill(json=[run]))
page.route(f'{harvestview_server_url}/api/v1/runs/vhost-run', lambda route: route.fulfill(json=run))
page.goto(f'{harvestview_server_url}/')
expect(page.get_by_role('button', name='Hostnames 2')).to_be_enabled()
expect(page.get_by_role('button', name='Virtual hosts 1')).to_have_count(0)
expect(page.locator('#route-count')).to_have_text('2')
expect(page.locator('#results-summary')).to_have_text('2 normalized results across 1 route.')
result_row = page.locator('.tabulator-row').first
expect(result_row).to_contain_text('admin.example.com')
expect(result_row).to_contain_text(
'https://192.0.2.10:443/ · HTTP 200 · body_sha256 · IP HTTP 200 · unknown HTTP 200 · body confirmed · TLS verified'
)
expect(result_row).to_contain_text(
'http://198.51.100.20:80/ · HTTP 302 → /login · status, location · IP HTTP 404 · unknown HTTP 404'
)
expect(result_row).to_contain_text('vhost')
expect(page.get_by_role('button', name='Take screenshot of admin.example.com (P2)')).to_be_visible()
expect(page.get_by_role('button', name='DNS brute force admin.example.com (P1)')).to_be_visible()
def test_harvestview_renders_and_filters_takeover_outcomes(
harvestview_server_url: str,
page: Page,
) -> None:
run = {
'run_id': 'takeover-run',
'target': 'example.test',
'status': 'completed',
'origin': 'local',
'created_at': '2026-08-15T12:00:00+00:00',
'started_at': '2026-08-15T12:00:01+00:00',
'completed_at': '2026-08-15T12:00:05+00:00',
'cancellation_requested_at': None,
'evidence_status': 'complete',
'result_count': 2,
'activities': ['P0', 'P2'],
'sources': ['crtsh'],
'request': {'sources': ['crtsh'], 'limit': 25, 'deadline_seconds': 300, 'takeover': True},
'source_executions': [],
'action_executions': [
{'action': 'takeover', 'status': 'partial', 'result_count': 2, 'duration_ms': 125},
],
'results': [
{
'type': 'takeover',
'value': 'bucket.example.test',
'sources': [],
'actions': ['takeover'],
'details': {
'status': 'indicator',
'dns': [
{
'resolver': '1.1.1.1',
'cname_chain': ['missing-bucket.s3.amazonaws.com'],
'terminal_rcode': 'NOERROR',
'error_type': None,
}
],
'wildcard_dns': [
{
'resolver': '1.1.1.1',
'cname_chain': [],
'terminal_rcode': 'NXDOMAIN',
'error_type': None,
}
],
'http': [
{
'scheme': 'https',
'status': 404,
'location': None,
'error_type': None,
'body_truncated': False,
}
],
'indicators': [
{
'classification': 'vulnerable-indicator',
'service': 'AWS/S3',
'rule_id': 'aws-s3',
'rule_revision': 'takeover-rules-v1',
'scheme': 'https',
'matched': ['body:BucketName', 'body:The specified bucket does not exist'],
}
],
'error_types': [],
},
},
{
'type': 'takeover',
'value': 'uncertain.example.test',
'sources': [],
'actions': ['takeover'],
'details': {
'status': 'inconclusive',
'dns': [
{
'resolver': '8.8.8.8',
'cname_chain': [],
'terminal_rcode': 'ERROR',
'error_type': 'DNSTimeoutError',
}
],
'wildcard_dns': [],
'http': [],
'indicators': [],
'error_types': ['DNSTimeoutError'],
},
},
],
'screenshots': [],
'log': '',
'error': None,
}
page.route(f'{harvestview_server_url}/api/v1/runs', lambda route: route.fulfill(json=[run]))
page.route(f'{harvestview_server_url}/api/v1/runs/takeover-run', lambda route: route.fulfill(json=run))
page.goto(f'{harvestview_server_url}/')
page.get_by_role('button', name='Takeover outcomes 2').click()
rows = page.locator('.tabulator-row')
expect(rows).to_have_count(2)
expect(rows.first).to_contain_text('bucket.example.test')
expect(rows.first).to_contain_text('vulnerable indicator · AWS/S3 · aws-s3@takeover-rules-v1 · HTTPS')
expect(rows.first).to_contain_text('Candidate 1.1.1.1 · CNAME missing-bucket.s3.amazonaws.com · NOERROR')
expect(rows.first).to_contain_text('Wildcard control 1.1.1.1 · No CNAME · NXDOMAIN')
expect(rows.first).to_contain_text('HTTPS · HTTP 404')
expect(rows.first).not_to_contain_text('DNS: not captured')
page.get_by_placeholder('Filter status or errors').press_sequentially('DNSTimeoutError')
visible_rows = page.locator('.tabulator-row:visible')
expect(visible_rows).to_have_count(1)
expect(visible_rows.first).to_contain_text('uncertain.example.test')
expect(visible_rows.first).to_contain_text('inconclusive')
expect(visible_rows.first).to_contain_text('DNSTimeoutError')
def test_harvestview_renders_sourced_asn_organization_attribution(
harvestview_server_url: str,
page: Page,
) -> None:
run = {
'run_id': 'asn-attribution-run',
'target': 'example.test',
'status': 'completed',
'origin': 'local',
'created_at': '2026-08-12T12:00:00+00:00',
'started_at': '2026-08-12T12:00:01+00:00',
'completed_at': '2026-08-12T12:00:05+00:00',
'cancellation_requested_at': None,
'evidence_status': 'complete',
'result_count': 2,
'activities': ['P0'],
'sources': ['urlscan'],
'request': {'sources': ['urlscan'], 'limit': 25, 'deadline_seconds': 300},
'source_executions': [
{'source': 'urlscan', 'status': 'completed', 'result_count': 2, 'duration_ms': 125},
],
'action_executions': [],
'results': [
{
'type': 'asn',
'value': 'AS64500',
'sources': ['urlscan'],
'actions': [],
'observations': [
{
'type': 'organization-attribution',
'producer_kind': 'source',
'producer': 'urlscan',
'organization_label': 'Example Transit',
'subject': {'type': 'hostname', 'value': 'api.example.test'},
'collected_at': '2026-08-12T12:00:03Z',
}
],
},
{
'type': 'hostname',
'value': 'api.example.test',
'sources': ['urlscan'],
'actions': [],
},
],
'screenshots': [],
'log': '',
'error': None,
}
page.route(f'{harvestview_server_url}/api/v1/runs', lambda route: route.fulfill(json=[run]))
page.route(f'{harvestview_server_url}/api/v1/runs/asn-attribution-run', lambda route: route.fulfill(json=run))
page.goto(f'{harvestview_server_url}/')
page.get_by_role('button', name='ASNs 1').click()
result_row = page.locator('.tabulator-row').first
expect(result_row).to_contain_text('AS64500')
expect(result_row).to_contain_text('Example Transit · source:urlscan · hostname:api.example.test')
def test_harvestview_summarizes_routeviews_prefix_evidence(
harvestview_server_url: str,
page: Page,
) -> None:
run = {
'run_id': 'routeviews-run',
'target': 'example.test',
'status': 'completed',
'origin': 'local',
'created_at': '2026-08-12T12:00:00+00:00',
'started_at': '2026-08-12T12:00:01+00:00',
'completed_at': '2026-08-12T12:00:05+00:00',
'cancellation_requested_at': None,
'evidence_status': 'complete',
'result_count': 2,
'activities': ['P0'],
'sources': ['urlscan'],
'request': {'sources': ['urlscan'], 'limit': 25, 'deadline_seconds': 300, 'routeviews': True},
'source_executions': [],
'action_executions': [
{'action': 'routeviews', 'status': 'completed', 'result_count': 2, 'duration_ms': 125},
],
'results': [
{
'type': 'asn',
'value': 'AS64500',
'sources': [],
'actions': ['routeviews'],
},
{
'type': 'prefix',
'value': '192.0.2.0/24',
'scope': 'external-relationship',
'sources': [],
'actions': ['routeviews'],
'observations': [
{
'type': 'observed-origin',
'action': 'routeviews',
'origin_asn': 'AS64500',
'collected_at': '2026-08-12T12:00:03Z',
},
{
'type': 'rpki-validation',
'action': 'routeviews',
'origin_asn': 'AS64500',
'state': 'valid',
'observed_at': '2026-08-12T11:59:00Z',
'collected_at': '2026-08-12T12:00:03Z',
},
{
'type': 'bgp-route',
'action': 'routeviews',
'origin_asn': 'AS64500',
'collector': 'route-views.example',
'peer_asn': 'AS64496',
'peer_address': '198.51.100.1',
'as_path': '64496 64500',
'communities': '64500:1',
'observed_at': '2026-08-12T11:59:00Z',
'collected_at': '2026-08-12T12:00:03Z',
},
],
},
],
'screenshots': [],
'log': '',
'error': None,
}
page.route(f'{harvestview_server_url}/api/v1/runs', lambda route: route.fulfill(json=[run]))
page.route(f'{harvestview_server_url}/api/v1/runs/routeviews-run', lambda route: route.fulfill(json=run))
page.goto(f'{harvestview_server_url}/')
page.get_by_role('button', name='Network prefixes 1').click()
expect(page.locator('#request-options')).to_contain_text('RouteViews enrichmentSelected')
result_row = page.locator('.tabulator-row').first
expect(result_row).to_contain_text('192.0.2.0/24')
expect(result_row).to_contain_text('AS64500 · RPKI valid')
expect(result_row.get_by_text('1 BGP route observation')).to_be_visible()
result_row.get_by_text('1 BGP route observation').click()
expect(result_row).to_contain_text('route-views.example · peer AS64496 (198.51.100.1)')
expect(result_row).to_contain_text('path 64496 64500 · communities 64500:1')
@pytest.mark.parametrize(
('viewport_width', 'viewport_height'),
[(1440, 900), (1024, 768), (820, 1180), (390, 844)],
ids=['desktop', 'desktop-compact', 'tablet', 'mobile'],
)
def test_hostname_actions_queue_isolated_runs(
harvestview_server_url: str,
page: Page,
browser_failures,
viewport_width: int,
viewport_height: int,
) -> None:
page.set_viewport_size({'width': viewport_width, 'height': viewport_height})
browser_failures.allow_response('POST', 503, '/api/v1/runs')
browser_failures.allow_response('POST', 503, '/api/v1/runs')
browser_failures.allow_console_error(
'Failed to load resource: the server responded with a status of 503 (Service Unavailable)'
)
browser_failures.allow_console_error(
'Failed to load resource: the server responded with a status of 503 (Service Unavailable)'
)
run = {
'run_id': 'parent-run',
'target': 'example.com',
'status': 'completed',
'origin': 'local',
'created_at': '2026-08-05T12:00:00+00:00',
'started_at': '2026-08-05T12:00:01+00:00',
'completed_at': '2026-08-05T12:00:05+00:00',
'cancellation_requested_at': None,
'evidence_status': 'complete',
'result_count': 1,
'activities': ['P0'],
'sources': ['crtsh'],
'request': {
'sources': ['crtsh'],
'limit': 25,
'deadline_seconds': 300,
'dns_resolvers': ['192.0.2.53'],
},
'source_executions': [],
'action_executions': [
{
'action': 'dns-brute',
'status': 'failed',
'result_count': 0,
'duration_ms': 125,
'error_type': 'TimeoutError',
'stop_reason': 'query-errors',
}
],
'results': [
{
'type': 'hostname',
'value': 'api.example.com',
'observations': [{'endpoint': 'https://192.0.2.10', 'status': 200}],
}
],
'screenshots': [],
'log': '',
'error': None,
}
submissions: list[dict[str, object]] = []
def route_runs(route: Route) -> None:
if route.request.method == 'POST':
submission = route.request.post_data_json
submissions.append(submission)
detail = 'Screenshot captured' if submission.get('screenshot') else 'DNS brute captured'
route.fulfill(status=503, json={'detail': detail})
else:
route.fulfill(json=[run])
page.route(f'{harvestview_server_url}/api/v1/runs', route_runs)
page.route(f'{harvestview_server_url}/api/v1/runs/parent-run', lambda route: route.fulfill(json=run))
page.goto(f'{harvestview_server_url}/')
expect(page.get_by_role('button', name='Hostnames 1')).to_be_enabled()
assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth')
page.locator('#provider-details summary').click()
expect(page.locator('#provider-title')).to_have_text('Execution outcomes')
action_row = page.locator('#provider-body tr').filter(has_text='dns-brute')
expect(action_row).to_contain_text('Action')
expect(action_row).to_contain_text('failed')
expect(action_row).to_contain_text('TimeoutError')
collapsed_actions = page.locator('.tabulator-responsive-collapse').get_by_text('Actions', exact=True)
expect(collapsed_actions).to_be_visible()
page.get_by_role('button', name='Take screenshot of api.example.com (P2)').click()
review = page.locator('#result-action-dialog')
expect(review.get_by_role('heading')).to_have_text('Review screenshot interaction')
expect(review).to_contain_text('api.example.com')
expect(review).to_contain_text('P2 · Direct interaction')
expect(review).to_contain_text('Creates a separate finite run')
assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth')
assert submissions == []
review.get_by_role('button', name='Start screenshot run').click()
expect(page.locator('#toast')).to_contain_text('Screenshot captured')
page.get_by_role('button', name='DNS brute force api.example.com (P1)').click()
expect(review.get_by_role('heading')).to_have_text('Review DNS brute force interaction')
expect(review).to_contain_text('P1 · DNS interaction')
expect(review).to_contain_text('192.0.2.53')
review.get_by_role('button', name='Start DNS brute force run').click()
expect(page.locator('#toast')).to_contain_text('DNS brute captured')
assert submissions == [
{'target': 'api.example.com', 'sources': [], 'screenshot': True},
{
'target': 'api.example.com',
'sources': [],
'dns_brute': True,
'dns_resolvers': ['192.0.2.53'],
},
]
assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth')
def test_accepted_result_action_is_not_reported_as_failed_when_refresh_fails(
harvestview_server_url: str,
page: Page,
browser_failures,
) -> None:
browser_failures.allow_response('GET', 503, '/api/v1/runs')
browser_failures.allow_console_error(
'Failed to load resource: the server responded with a status of 503 (Service Unavailable)'
)
run = {
'run_id': 'parent-run',
'target': 'example.com',
'status': 'running',
'origin': 'local',
'created_at': '2026-08-05T12:00:00+00:00',
'started_at': '2026-08-05T12:00:01+00:00',
'completed_at': None,
'cancellation_requested_at': None,
'evidence_status': 'partial',
'result_count': 1,
'activities': ['P0'],
'sources': ['crtsh'],
'request': {'sources': ['crtsh'], 'limit': 25, 'deadline_seconds': 300},
'source_executions': [],
'action_executions': [],
'results': [{'type': 'hostname', 'value': 'api.example.com'}],
'screenshots': [],
'log': '',
'error': None,
}
list_calls = 0
post_calls = 0
cancelled_ids: list[str] = []
def route_runs(route: Route) -> None:
nonlocal list_calls, post_calls
if route.request.method == 'POST':
post_calls += 1
route.fulfill(status=201, json={'run_id': 'queued-action', 'target': 'api.example.com', 'status': 'queued'})
return
list_calls += 1
if list_calls == 1:
route.fulfill(json=[run])
elif list_calls == 2:
route.fulfill(status=503, json={'detail': 'Refresh unavailable'})
else:
route.fulfill(json=[{**run, 'status': 'cancelled', 'completed_at': '2026-08-05T12:00:06+00:00'}])
def route_cancel(route: Route, run_id: str) -> None:
cancelled_ids.append(run_id)
route.fulfill(json={**run, 'status': 'cancelled', 'completed_at': '2026-08-05T12:00:06+00:00'})
page.route(f'{harvestview_server_url}/api/v1/runs', route_runs)
page.route(
f'{harvestview_server_url}/api/v1/runs/parent-run/cancel',
lambda route: route_cancel(route, 'parent-run'),
)
page.route(
f'{harvestview_server_url}/api/v1/runs/queued-action/cancel',
lambda route: route_cancel(route, 'queued-action'),
)
page.route(f'{harvestview_server_url}/api/v1/runs/parent-run', lambda route: route.fulfill(json=run))
page.goto(f'{harvestview_server_url}/')
page.get_by_role('button', name='Take screenshot of api.example.com (P2)').click()
page.locator('#result-action-dialog').get_by_role('button', name='Start screenshot run').click()
expect(page.locator('#toast')).to_contain_text('was queued, but the run view could not refresh')
expect(page.locator('#toast')).to_contain_text('Do not submit it again')
expect(page.locator('#detail-run-id')).to_have_text('parent-run')
page.locator('#cancel-run-button').click()
expect(page.locator('#toast')).to_have_text('Queued enumeration cancelled.')
assert post_calls == 1
assert cancelled_ids == ['parent-run']
def test_accepted_cancellation_is_not_reported_as_failed_when_history_refresh_fails(
harvestview_server_url: str,
page: Page,
browser_failures,
) -> None:
browser_failures.allow_response('GET', 503, '/api/v1/runs')
browser_failures.allow_console_error(
'Failed to load resource: the server responded with a status of 503 (Service Unavailable)'
)
run = {
'run_id': 'running-run',
'target': 'example.com',
'status': 'running',
'origin': 'local',
'created_at': '2026-08-05T12:00:00+00:00',
'started_at': '2026-08-05T12:00:01+00:00',
'completed_at': None,
'cancellation_requested_at': None,
'evidence_status': 'partial',
'result_count': 0,
'activities': ['P0'],
'sources': ['crtsh'],
'request': {'sources': ['crtsh'], 'limit': 25, 'deadline_seconds': 300},
'source_executions': [],
'action_executions': [],
'results': [],
'screenshots': [],
'log': '',
'error': None,
}
list_calls = 0
cancel_calls = 0
def route_runs(route: Route) -> None:
nonlocal list_calls
list_calls += 1
if list_calls == 1:
route.fulfill(json=[run])
else:
route.fulfill(status=503, json={'detail': 'Refresh unavailable'})
def route_cancel(route: Route) -> None:
nonlocal cancel_calls
cancel_calls += 1
route.fulfill(json={**run, 'status': 'cancelling', 'cancellation_requested_at': '2026-08-05T12:00:02+00:00'})
page.route(f'{harvestview_server_url}/api/v1/runs', route_runs)
page.route(f'{harvestview_server_url}/api/v1/runs/running-run/cancel', route_cancel)
page.route(f'{harvestview_server_url}/api/v1/runs/running-run', lambda route: route.fulfill(json=run))
page.goto(f'{harvestview_server_url}/')
page.locator('#cancel-run-button').click()
expect(page.locator('#toast')).to_contain_text('Cancellation was accepted, but run history could not refresh')
expect(page.locator('#toast')).to_contain_text('Do not request it again')
assert cancel_calls == 1
def test_harvestview_can_select_sources_by_result_capability(harvestview_server_url: str, page: Page) -> None:
page.goto(f'{harvestview_server_url}/')
catalog = page.context.request.get(f'{harvestview_server_url}/api/v1/sources').json()
expected = {source['name'] for source in catalog['sources'] if source['ready'] and 'ips' in source['capabilities']}
page.get_by_role('button', name='Start enumeration').first.click()
page.get_by_role('button', name='Clear', exact=True).click()
page.locator('#source-capability').select_option('ips')
page.get_by_role('button', name='Add sources').click()
selected = set(page.locator('#source-groups input:checked').evaluate_all('(inputs) => inputs.map(input => input.value)'))
assert selected == expected
def test_polling_recovers_after_one_transient_refresh_failure(
harvestview_server_url: str,
page: Page,
browser_failures,
) -> None:
browser_failures.allow_response('GET', 503, '/api/v1/runs/retry-run')
browser_failures.allow_console_error(
'Failed to load resource: the server responded with a status of 503 (Service Unavailable)'
)
run = {
'run_id': 'retry-run',
'target': 'example.com',
'status': 'running',
'origin': 'local',
'created_at': '2026-08-05T12:00:00+00:00',
'started_at': '2026-08-05T12:00:01+00:00',
'completed_at': None,
'cancellation_requested_at': None,
'evidence_status': 'partial',
'result_count': 0,
'activities': ['P0'],
'sources': ['crtsh'],
'request': {'sources': ['crtsh'], 'limit': 25, 'deadline_seconds': 300},
'source_executions': [],
'results': [],
'screenshots': [],
'log': '',
'error': None,
}
detail_calls = 0
def route_runs(route: Route) -> None:
route.fulfill(json=[run])
def route_detail(route: Route) -> None:
nonlocal detail_calls
detail_calls += 1
if detail_calls == 2:
route.fulfill(status=503, json={'detail': 'Temporary refresh failure'})
else:
completed = detail_calls >= 3
route.fulfill(
json={
**run,
'status': 'completed' if completed else 'running',
'completed_at': '2026-08-05T12:00:05+00:00' if completed else None,
'evidence_status': 'complete' if completed else 'partial',
}
)
page.route(f'{harvestview_server_url}/api/v1/runs', route_runs)
page.route(f'{harvestview_server_url}/api/v1/runs/retry-run', route_detail)
page.goto(f'{harvestview_server_url}/')
expect(page.locator('#status-chips')).to_contain_text('completed', timeout=7_000)
assert detail_calls >= 3
def test_workspace_startup_failure_has_an_inline_retry(
harvestview_server_url: str,
page: Page,
browser_failures,
) -> None:
browser_failures.allow_response('GET', 503, '/api/v1/sources')
browser_failures.allow_console_error(
'Failed to load resource: the server responded with a status of 503 (Service Unavailable)'
)
source_calls = 0
def fail_once(route: Route) -> None:
nonlocal source_calls
source_calls += 1
if source_calls == 1:
route.fulfill(status=503, json={'detail': 'Temporary startup failure'})
else:
route.continue_()
page.route(f'{harvestview_server_url}/api/v1/sources', fail_once)
page.goto(f'{harvestview_server_url}/')
page.set_default_timeout(3_000)
expect(page.locator('#workspace-error')).to_be_visible()
expect(page.locator('#workspace-error-message')).to_have_text('Temporary startup failure')
page.locator('#retry-workspace-button').click()
expect(page.get_by_role('heading', name='No enumeration runs yet')).to_be_visible()
assert source_calls == 2
def test_unchanged_poll_keeps_result_table_filters(harvestview_server_url: str, page: Page) -> None:
run = {
'run_id': 'stable-run',
'target': 'example.com',
'status': 'running',
'origin': 'local',
'created_at': '2026-08-05T12:00:00+00:00',
'started_at': '2026-08-05T12:00:01+00:00',
'completed_at': None,
'cancellation_requested_at': None,
'evidence_status': 'partial',
'result_count': 1,
'activities': ['P0', 'P2'],
'sources': ['crtsh'],
'request': {
'sources': ['crtsh'],
'limit': 25,
'deadline_seconds': 300,
'proxies': True,
'dns_lookup': True,
'takeover': True,
},
'source_executions': [],
'results': [{'type': 'hostname', 'value': 'api.example.com'}],
'screenshots': [],
'log': '',
'error': None,
}
page.route(f'{harvestview_server_url}/api/v1/runs', lambda route: route.fulfill(json=[run]))
page.route(f'{harvestview_server_url}/api/v1/runs/stable-run', lambda route: route.fulfill(json=run))
page.goto(f'{harvestview_server_url}/')
expect(page.locator('#request-options')).to_contain_text('HTTP(S) proxy transportSelected')
expect(page.locator('#request-options')).to_contain_text('DNS lookup (/24 reverse expansion)Selected')
expect(page.locator('#request-options')).to_contain_text('Takeover transportDNS resolvers + HTTP proxy')
value_filter = page.locator('.tabulator-col[tabulator-field="value"] .tabulator-header-filter input')
value_filter.fill('api')
page.wait_for_timeout(1_600)
expect(value_filter).to_have_value('api')
@pytest.mark.parametrize('initial_detail_status', ['queued', 'running'])
def test_submission_notification_matches_the_loaded_lifecycle(
harvestview_server_url: str,
page: Page,
initial_detail_status: str,
) -> None:
page.goto(f'{harvestview_server_url}/')
queued_run = {
'run_id': 'toast-run',
'target': 'example.com',
'status': 'queued',
'origin': 'local',
'created_at': '2026-08-05T12:00:00+00:00',
'started_at': None,
'completed_at': None,
'cancellation_requested_at': None,
'evidence_status': 'partial',
'result_count': 0,
'activities': ['P0'],
'sources': ['crtsh'],
'request': {'sources': ['crtsh'], 'limit': 25, 'deadline_seconds': 300},
'source_executions': [],
'results': [],
'screenshots': [],
'log': '',
'error': None,
}
detail_status = initial_detail_status
def route_runs(route: Route) -> None:
if route.request.method == 'POST':
route.fulfill(status=201, json=queued_run)
else:
route.fulfill(json=[{**queued_run, 'status': 'running', 'started_at': '2026-08-05T12:00:01+00:00'}])
def route_detail(route: Route) -> None:
if detail_status == 'queued':
run = queued_run
elif detail_status == 'running':
run = {
**queued_run,
'status': 'running',
'started_at': '2026-08-05T12:00:01+00:00',
}
else:
run = {
**queued_run,
'status': 'cancelling',
'started_at': '2026-08-05T12:00:01+00:00',
'cancellation_requested_at': '2026-08-05T12:00:02+00:00',
}
route.fulfill(json=run)
page.route(f'{harvestview_server_url}/api/v1/runs', route_runs)
page.route(f'{harvestview_server_url}/api/v1/runs/toast-run', route_detail)
page.get_by_role('button', name='Start enumeration').first.click()
page.locator('#run-target').fill('example.com')
page.locator('#run-limit').fill('25')
page.locator('#run-deadline').fill('300')
page.locator('#submit-run-button').click()
if initial_detail_status == 'queued':
expect(page.locator('#toast')).to_have_text('Enumeration for example.com is queued.')
detail_status = 'running'
expect(page.locator('#status-chips')).to_contain_text('running')
expect(page.locator('#submit-run-button')).to_be_enabled()
expect(page.locator('#toast')).to_be_hidden(timeout=2500)
detail_status = 'cancelling'
expect(page.locator('#lifecycle-track strong')).to_contain_text(['Cancellation requested'])
@pytest.mark.parametrize(
('terminal_status', 'title', 'copy'),
[
('cancelled', 'Enumeration cancelled', 'The enumeration was cancelled.'),
('failed', 'Enumeration failed', 'Provider process exited.'),
],
)
def test_empty_terminal_run_explains_its_lifecycle(
harvestview_server_url: str,
page: Page,
terminal_status: str,
title: str,
copy: str,
) -> None:
run = {
'run_id': f'{terminal_status}-run',
'target': 'example.com',
'status': terminal_status,
'origin': 'local',
'created_at': '2026-08-05T12:00:00+00:00',
'started_at': '2026-08-05T12:00:01+00:00',
'completed_at': '2026-08-05T12:00:02+00:00',
'cancellation_requested_at': None,
'evidence_status': 'partial',
'result_count': 0,
'activities': ['P0'],
'sources': ['crtsh'],
'request': {'sources': ['crtsh'], 'limit': 25, 'deadline_seconds': 300},
'source_executions': [],
'results': [],
'screenshots': [],
'log': '',
'error': 'Provider process exited.' if terminal_status == 'failed' else None,
}
page.route(f'{harvestview_server_url}/api/v1/runs', lambda route: route.fulfill(json=[run]))
page.route(f'{harvestview_server_url}/api/v1/runs/{run["run_id"]}', lambda route: route.fulfill(json=run))
page.goto(f'{harvestview_server_url}/')
expect(page.locator('#results-empty-title')).to_have_text(title)
expect(page.locator('#results-empty-copy')).to_contain_text(copy)
expect(page.locator('#results-empty-copy')).to_contain_text('The retained evidence record is partial.')
def test_failed_run_with_retained_evidence_puts_assessment_before_results(
harvestview_server_url: str,
page: Page,
) -> None:
run = {
'run_id': 'failed-evidence-run',
'target': 'example.com',
'status': 'failed',
'origin': 'local',
'created_at': '2026-08-05T12:00:00+00:00',
'started_at': '2026-08-05T12:00:01+00:00',
'completed_at': '2026-08-05T12:00:02+00:00',
'cancellation_requested_at': None,
'evidence_status': 'partial',
'result_count': 1,
'activities': ['P0'],
'sources': ['crtsh'],
'request': {'sources': ['crtsh'], 'limit': 25, 'deadline_seconds': 300},
'source_executions': [
{
'source': 'crtsh',
'status': 'partial',
'result_count': 1,
'duration_ms': 1000,
'error_type': 'TimeoutError',
'stop_reason': 'timeout',
}
],
'results': [{'type': 'hostname', 'value': 'api.example.com', 'sources': ['crtsh']}],
'screenshots': [],
'log': '',
'error': 'Provider process exited.',
}
page.route(f'{harvestview_server_url}/api/v1/runs', lambda route: route.fulfill(json=[run]))
page.route(f'{harvestview_server_url}/api/v1/runs/{run["run_id"]}', lambda route: route.fulfill(json=run))
page.goto(f'{harvestview_server_url}/')
expect(page.locator('#assessment-evidence')).to_contain_text('Partial')
expect(page.get_by_role('button', name='Hostnames 1')).to_be_enabled()
assert page.locator('.run-assessment').evaluate(
"node => Boolean(node.compareDocumentPosition(document.querySelector('#results-section')) & Node.DOCUMENT_POSITION_FOLLOWING)"
)
def test_completed_empty_import_explains_terminal_outcome(
harvestview_server_url: str,
page: Page,
tmp_path: Path,
) -> None:
evidence_file = tmp_path / 'empty-run.jsonl'
write_jsonl_evidence(
evidence_file,
{
'run_id': '4ce79bb1-91a1-4456-8589-e5d82b55f2b4',
'target': 'example.com',
'started_at': '2026-08-05T12:00:00+00:00',
'completed_at': '2026-08-05T12:00:25+00:00',
'status': 'complete',
'source_executions': [
{'source': 'crtsh', 'status': 'completed', 'result_count': 0, 'duration_ms': 25000},
],
'results': [],
},
)
page.goto(f'{harvestview_server_url}/')
page.get_by_role('button', name='Import result file').first.click()
page.locator('#result-file').set_input_files(evidence_file)
page.locator('#submit-import-button').click()
expect(page.locator('#results-empty-title')).to_have_text('Enumeration completed')
expect(page.locator('#results-summary')).to_have_text(
'0 normalized results · 1 completed (1 zero-result) / 0 partial / 0 skipped / 0 failed.'
)
expect(page.locator('#results-empty-copy')).to_have_text(
'crtsh returned no normalized evidence. The retained evidence record is complete.'
)
expect(page.locator('#lifecycle-track strong')).to_have_text(['Original started', 'Original completed', 'Imported'])
expect(page.get_by_role('button', name='Export all JSONL')).to_be_enabled()
with page.expect_download() as jsonl_download:
page.get_by_role('button', name='Export all JSONL').click()
exported_records = [json.loads(line) for line in Path(jsonl_download.value.path()).read_text(encoding='utf-8').splitlines()]
assert len(exported_records) == 1
assert exported_records[0]['evidence_status'] == 'complete'
assert exported_records[0]['result_count'] == 0
assert exported_records[0]['source_executions'][0]['status'] == 'completed'
def test_harvestview_exports_and_reimports_completed_runs_as_sqlite(
harvestview_server,
page: Page,
tmp_path: Path,
) -> None:
harvestview_server_url = harvestview_server.url
source_run_id = '4ef278df-ce95-4120-9241-6e71dd96ad74'
evidence_file = tmp_path / 'completed-run.jsonl'
write_jsonl_evidence(
evidence_file,
{
'run_id': source_run_id,
'target': 'sqlite.example.test',
'started_at': '2026-08-08T01:00:00Z',
'completed_at': '2026-08-08T01:01:00Z',
'status': 'complete',
'source_executions': [{'source': 'crtsh', 'status': 'completed', 'duration_ms': 2, 'result_count': 1}],
'action_executions': [{'action': 'vhost', 'status': 'completed', 'duration_ms': 1, 'result_count': 1}],
'results': [
{
'type': 'hostname',
'value': 'admin.sqlite.example.test',
'sources': ['crtsh'],
'actions': ['vhost'],
'observations': [
{
'endpoint': 'https://192.0.2.8:443/',
'http_host': 'admin.sqlite.example.test',
'tls_server_name': 'admin.sqlite.example.test',
'classification': 'distinct',
'phase': 'body',
'status': 401,
'location': None,
'body_sha256': 'a' * 64,
'body_size': 12,
'body_truncated': False,
'context_phase': 'body',
'context_status': 200,
'context_location': None,
'context_body_sha256': 'a' * 64,
'context_body_size': 12,
'context_body_truncated': False,
'control_phase': 'body',
'control_status': 200,
'control_location': None,
'control_body_sha256': 'a' * 64,
'control_body_size': 12,
'control_body_truncated': False,
'confirmation_body_sha256': None,
'tls_verified': True,
'distinct_signals': ['status'],
'reflection_normalized': False,
}
],
}
],
},
)
page.goto(f'{harvestview_server_url}/')
page.get_by_role('button', name='Import result file').first.click()
page.locator('#result-file').set_input_files(evidence_file)
page.locator('#submit-import-button').click()
expect(page.locator('#detail-target')).to_have_text('sqlite.example.test')
expect(page.locator('#run-count')).to_have_text('1')
expect(page.locator('#toast')).to_have_text('Imported completed-run.jsonl without executing discovery.')
expect(page.get_by_role('button', name='Hostnames 1')).to_be_enabled()
imported_run_id = page.locator('#detail-run-id').inner_text()
with page.expect_download() as database_download:
page.get_by_role('button', name='Export database').click()
assert database_download.value.suggested_filename == 'theharvester-completed-runs.sqlite'
exported_database = Path(database_download.value.path())
portable_database = tmp_path / database_download.value.suggested_filename
portable_database.write_bytes(exported_database.read_bytes())
assert portable_database.read_bytes().startswith(b'SQLite format 3\x00')
with sqlite3.connect(portable_database) as connection:
exported_run_ids = {row[0] for row in connection.execute('SELECT run_id FROM runs')}
exported_tables = {row[0] for row in connection.execute("SELECT name FROM sqlite_master WHERE type = 'table'")}
assert exported_run_ids == {imported_run_id}
assert {'run_records', 'run_worker_leases'}.isdisjoint(exported_tables)
harvestview_server.stop()
database = Path(harvestview_server.environment['THEHARVESTER_RUN_DB'])
for database_file in (database, Path(f'{database}-wal'), Path(f'{database}-shm')):
database_file.unlink(missing_ok=True)
harvestview_server.start()
page.goto(f'{harvestview_server_url}/')
expect(page.get_by_role('heading', name='No enumeration runs yet')).to_be_visible()
page.get_by_role('button', name='Import result file').first.click()
page.locator('#result-file').set_input_files(portable_database)
page.locator('#submit-import-button').click()
expect(page.locator('#toast')).to_have_text('Imported 1 run from theharvester-completed-runs.sqlite; 0 already present.')
expect(page.locator('#detail-run-id')).to_have_text(imported_run_id)
expect(page.locator('#detail-target')).to_have_text('sqlite.example.test')
result_row = page.locator('.tabulator-row').first
expect(result_row).to_contain_text('admin.sqlite.example.test')
expect(result_row).to_contain_text('https://192.0.2.8:443/ · HTTP 401 · status')
expect(result_row).to_contain_text('crtsh')
expect(result_row).to_contain_text('vhost')
def test_harvestview_can_import_and_analyze_fixture_evidence_through_the_real_ui(
harvestview_server_url: str,
page: Page,
tmp_path: Path,
) -> None:
page.set_viewport_size({'width': 1440, 'height': 900})
page.context.grant_permissions(['clipboard-read', 'clipboard-write'], origin=harvestview_server_url)
page.goto(f'{harvestview_server_url}/')
catalog = page.context.request.get(f'{harvestview_server_url}/api/v1/sources').json()
passive_sources = [source for source in catalog['sources'] if source['activity'] == 'P0']
credentialed = [source for source in passive_sources if source['credentials']]
uncredentialed = [source for source in passive_sources if not source['credentials']]
ordered_sources = uncredentialed[:23] + credentialed[:2] + uncredentialed[23:] + credentialed[2:]
assert len(ordered_sources) == len(passive_sources)
assert ordered_sources[23]['credentials']
assert ordered_sources[24]['credentials']
evidence = {
'run_id': '7bb74ee1-c81c-4ccd-8ec7-e8e496490f53',
'target': 'example.com',
'started_at': '2026-08-04T12:00:01+00:00',
'completed_at': '2026-08-04T12:03:21+00:00',
'status': 'partial',
'source_executions': [
{
'source': source['name'],
'status': 'completed' if index < 23 else 'skipped',
'result_count': 1 if index < 3 else 0,
'duration_ms': index + 1,
**({'error_type': 'SourceDidNotStart'} if index >= 23 else {}),
**({'stop_reason': 'missing-credentials'} if index == 23 else {}),
}
for index, source in enumerate(ordered_sources)
],
'results': [
{
'type': result_type,
'value': (
'192.0.2.10' if result_type == 'ip' else 'AS64500' if result_type == 'asn' else f'{result_type}.example.com'
),
'sources': [ordered_sources[index]['name']] if index < 3 else [],
}
for index, result_type in enumerate(('hostname', 'ip', 'asn', 'email', 'url', 'framework', 'person', 'language'))
]
+ [
{
'type': 'ip',
'value': '198.51.100.10',
}
],
}
evidence_file = tmp_path / 'broad-run.jsonl'
write_jsonl_evidence(evidence_file, evidence)
page.get_by_role('button', name='Import result file').first.click()
page.locator('#result-file').set_input_files(evidence_file)
page.locator('#submit-import-button').click()
expect(page.locator('#provider-outcome-summary')).to_have_text(
f'23 completed (20 zero-result) / 0 partial / {len(ordered_sources) - 23} skipped / 0 failed'
)
expect(page.locator('#assessment-evidence')).to_contain_text('Partial')
expect(page.locator('#assessment-evidence')).to_contain_text('9 retained results')
expect(page.locator('#assessment-producers')).to_have_text(f'23 of {len(ordered_sources)} completed')
expect(page.locator('#assessment-review')).to_contain_text(f'{len(ordered_sources) - 23} skipped')
page.locator('#review-outcomes-button').click()
expect(page.locator('#provider-details')).to_have_attribute('open', '')
expect(page.locator('#provider-details summary')).to_be_focused()
expect(page.locator('#run-count')).to_have_text('1')
expect(page.locator('#run-list button')).to_have_count(1)
assert page.locator('#results-title').evaluate(
"node => Boolean(node.compareDocumentPosition(document.querySelector('#provider-title')) & Node.DOCUMENT_POSITION_FOLLOWING)"
)
assert page.locator('#results-title').evaluate(
"node => Boolean(node.compareDocumentPosition(document.querySelector('#lifecycle-title')) & Node.DOCUMENT_POSITION_FOLLOWING)"
)
assert page.locator('.run-assessment').evaluate(
"node => Boolean(node.compareDocumentPosition(document.querySelector('#results-title')) & Node.DOCUMENT_POSITION_FOLLOWING)"
)
missing_credentials_row = page.locator('#provider-body tr').filter(has_text=ordered_sources[23]['name'])
expect(missing_credentials_row.locator('td').last).to_have_text(
'Required credentials were not configured; add them, then retry.'
)
unexpected_skip_reason = page.locator('#provider-body tr').filter(has_text=ordered_sources[24]['name']).locator('td').last
expect(unexpected_skip_reason).to_contain_text('Credentials required:')
expect(unexpected_skip_reason).to_contain_text('Source did not start')
page.locator('#history-search').fill('not-present')
expect(page.locator('#history-empty')).to_be_visible()
page.locator('#history-search').fill('')
ip_route = page.get_by_role('button', name='IP addresses 2')
ip_route.click()
expect(ip_route).to_have_attribute('tabindex', '0')
ip_route.focus()
ip_route.press('ArrowRight')
asn_route = page.get_by_role('button', name='ASNs 1')
expect(asn_route).to_have_attribute('aria-pressed', 'true')
expect(asn_route).to_be_focused()
asn_route.press('ArrowLeft')
expect(ip_route).to_be_focused()
route_buttons = page.locator('#route-tabs [data-route]')
assert page.locator('#route-tabs [data-route][tabindex="-1"]').count() == route_buttons.count() - 1
ip_route.press('End')
expect(route_buttons.last).to_be_focused()
expect(route_buttons.last).to_have_attribute('aria-pressed', 'true')
route_buttons.last.press('Home')
expect(route_buttons.first).to_be_focused()
expect(route_buttons.first).to_have_attribute('aria-pressed', 'true')
ip_route.click()
expect(ip_route).to_have_attribute('tabindex', '0')
expect(page.locator('#route-count')).to_have_text('2')
value_column = page.locator('.tabulator-col[tabulator-field="value"]')
value_filter = value_column.locator('.tabulator-header-filter input')
dns_filter = page.locator('.tabulator-col[tabulator-field="dns_status"] .tabulator-header-filter input')
expect(value_filter).to_have_attribute('placeholder', 'Filter values')
expect(dns_filter).to_have_attribute('placeholder', 'Filter DNS')
expect(value_filter).to_have_attribute('aria-label', 'Filter Value column')
expect(dns_filter).to_have_attribute('aria-label', 'Filter DNS column')
expect(page.get_by_role('checkbox', name='Select all rows on this route')).to_be_visible()
expect(page.get_by_role('checkbox', name='Select 192.0.2.10')).to_be_visible()
value_filter.press_sequentially('198.51')
expect(page.locator('.tabulator-row:visible')).to_have_count(1)
expect(page.locator('.tabulator-row:visible')).to_contain_text('198.51.100.10')
value_filter.press('ControlOrMeta+a')
value_filter.press('Backspace')
expect(page.locator('.tabulator-row:visible')).to_have_count(2)
dns_filter.press_sequentially('not captured')
expect(page.locator('.tabulator-row:visible')).to_have_count(2)
dns_filter.press('ControlOrMeta+a')
dns_filter.press('Backspace')
expect(page.locator('.tabulator-row:visible')).to_have_count(2)
resize_handles = page.locator('.tabulator-header .tabulator-col-resize-handle')
expect(resize_handles).to_have_count(2)
page.locator('#result-workbench').scroll_into_view_if_needed()
selection_column_width = page.locator('.tabulator-header .tabulator-row-header').bounding_box()['width']
assert 40 <= selection_column_width <= 56
resize_handle = resize_handles.first
initial_width = value_column.bounding_box()['width']
handle_box = resize_handle.bounding_box()
page.mouse.move(handle_box['x'] + handle_box['width'] / 2, handle_box['y'] + handle_box['height'] / 2)
page.mouse.down()
page.mouse.move(handle_box['x'] + handle_box['width'] / 2 + 80, handle_box['y'] + handle_box['height'] / 2)
page.mouse.up()
assert value_column.bounding_box()['width'] >= initial_width + 60
page.locator('.tabulator-row').first.click()
expect(page.locator('#copy-route-button')).to_have_text('Copy selected (1)')
page.locator('#copy-route-button').click()
expect(page.locator('#toast')).to_have_text('Copied 1 selected IP addresses.')
assert page.evaluate('navigator.clipboard.readText()') == '192.0.2.10'
with page.expect_download() as jsonl_download:
page.get_by_role('button', name='Export all JSONL').click()
exported_records = [json.loads(line) for line in Path(jsonl_download.value.path()).read_text(encoding='utf-8').splitlines()]
assert exported_records[0]['type'] == 'summary'
assert exported_records[0]['evidence_status'] == 'partial'
assert any(record['type'] == 'ip' and record['value'] == '192.0.2.10' for record in exported_records[1:])
page.get_by_role('button', name='Start enumeration').first.click()
expect(page.locator('#new-run-dialog').get_by_text('Credentials required:', exact=False).first).to_be_visible()
assert page.locator('.source-choice small').first.evaluate('node => parseFloat(getComputedStyle(node).fontSize)') >= 12
assert page.locator('.action-choice small').first.evaluate('node => parseFloat(getComputedStyle(node).fontSize)') >= 12
assert page.locator('#final-authorization-summary').evaluate('node => parseFloat(getComputedStyle(node).fontSize)') >= 12
page.get_by_role('button', name='Select all P0').click()
assert page.locator('[data-activity="P0"] input:checked').count() == len(
[source for source in passive_sources if source['ready']]
)
assert page.locator('[data-activity="P1"] input:checked, [data-activity="P2"] input:checked').count() == 0
assert page.locator('.action-grid input:checked').count() == 0
page.get_by_role('button', name='Clear', exact=True).click()
assert page.locator('[data-activity="P0"] input:checked').count() == 0
expect(page.locator('#activity-summary')).to_have_text('P0 off · P1 off · P2 off')
page.keyboard.press('Escape')
expect(page.get_by_role('button', name='Start enumeration').first).to_be_focused()
page.get_by_role('button', name='Hostnames 1').click()
page.set_viewport_size({'width': 820, 'height': 1180})
assert page.locator('.app-shell').evaluate("node => getComputedStyle(node).display === 'block'")
assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth')
page.set_viewport_size({'width': 390, 'height': 844})
assert page.locator('.version-badge').evaluate('node => parseFloat(getComputedStyle(node).fontSize)') >= 11
assert page.locator('.run-meta').first.evaluate('node => parseFloat(getComputedStyle(node).fontSize)') >= 12
assert page.locator('.source-choice small').first.evaluate('node => parseFloat(getComputedStyle(node).fontSize)') >= 16
assert page.locator('.action-choice small').first.evaluate('node => parseFloat(getComputedStyle(node).fontSize)') >= 16
assert page.locator('#final-authorization-summary').evaluate('node => parseFloat(getComputedStyle(node).fontSize)') >= 16
row_checkbox = page.get_by_role('checkbox', name='Select hostname.example.com').bounding_box()
header_checkbox = page.get_by_role('checkbox', name='Select all rows on this route').bounding_box()
assert row_checkbox['width'] >= 44
assert row_checkbox['height'] >= 44
assert header_checkbox['width'] >= 44
assert header_checkbox['height'] >= 44
expect(page.locator('#provider-outcome-summary')).to_be_visible()
expect(page.get_by_role('columnheader', name='Outcome')).to_be_visible()
expect(page.get_by_role('columnheader', name='Results')).to_be_hidden()
expect(page.locator('.tabulator-col[tabulator-field="value"]')).to_be_visible()
assert page.locator('.tabulator-col[tabulator-field="value"]').bounding_box()['width'] >= 250
expect(page.locator('#route-overflow-cue')).to_be_visible()
value_filter.scroll_into_view_if_needed()
expect(value_filter).to_be_visible()
assert value_filter.bounding_box()['height'] >= 44
assert page.locator('#route-tabs').evaluate('node => node.scrollWidth > node.clientWidth')
expect(page.get_by_role('button', name='Next Page')).to_be_visible()
workbench_box = page.locator('#result-workbench').bounding_box()
paginator_box = page.locator('.tabulator-paginator').bounding_box()
assert paginator_box['x'] + paginator_box['width'] <= workbench_box['x'] + workbench_box['width'] + 1