mirror of
https://github.com/laramies/theHarvester.git
synced 2026-09-28 12:34:54 +02:00
fix: harden hostname tracking release behavior
This commit is contained in:
+1
-1
@@ -56,7 +56,7 @@ When a change alters one of these boundaries, update this document and the neare
|
||||
|
||||
### Deferred boundaries
|
||||
|
||||
- Cross-run change detection, alerts, and automatic reactions remain separate future product decisions. A scheduled occurrence only submits finite enumeration runs.
|
||||
- Read-only cross-run change projections over finalized evidence are part of the release contract. Alerts and automatic reactions remain deferred, and a scheduled occurrence only submits finite enumeration runs.
|
||||
- Cross-run source ranking remains a reporting decision. One run's hostname-yield summary does not automatically select, disable, or rank sources.
|
||||
- Distributed workers, multi-host operation, PostgreSQL, and hosted multi-user authorization require measured demand and new decisions. The release remains SQLite-first and local-operator focused.
|
||||
- Automatic scope expansion remains deferred. Evidence can suggest a later target, while the operator controls every scope change.
|
||||
|
||||
@@ -533,6 +533,14 @@ def test_hostname_tracking_filters_persisted_run_changes(
|
||||
expect(panel).to_contain_text('TimeoutError')
|
||||
expect(panel).not_to_contain_text('stable.example.test')
|
||||
|
||||
page.locator('#tracking-change-filter').select_option('persisting')
|
||||
expect(page.locator('#tracking-persisting-filter')).to_be_checked()
|
||||
expect(rows).to_have_count(1)
|
||||
expect(panel).to_contain_text('stable.example.test')
|
||||
page.locator('#tracking-persisting-filter').uncheck()
|
||||
expect(page.locator('#tracking-change-filter')).to_have_value('')
|
||||
expect(rows).to_have_count(3)
|
||||
|
||||
page.locator('#tracking-exclusive-filter').check()
|
||||
expect(rows).to_have_count(2)
|
||||
expect(panel).not_to_contain_text('missing.example.test')
|
||||
|
||||
@@ -16,6 +16,7 @@ from theHarvester.lib.database import ResultStore
|
||||
from theHarvester.lib.evidence_types import RESULT_KINDS, ExecutionStatus
|
||||
|
||||
RUN_ONE = UUID('11111111-1111-4111-8111-111111111111')
|
||||
RUN_TIE_LATER = UUID('11111111-1111-4111-8111-111111111112')
|
||||
RUN_TWO = UUID('22222222-2222-4222-8222-222222222222')
|
||||
|
||||
|
||||
@@ -572,6 +573,42 @@ def test_target_changes_reports_every_run_pair_and_a_clear_null_baseline(
|
||||
assert {row['run_id'] for row in payload['hostname_changes']} == {str(RUN_TWO)}
|
||||
|
||||
|
||||
def test_run_id_breaks_an_equal_completion_time_tie_deterministically(
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
database = tmp_path / 'runs.sqlite'
|
||||
store = ResultStore(database)
|
||||
asyncio.run(store.initialize())
|
||||
asyncio.run(
|
||||
store.save_run(
|
||||
_completed_run(
|
||||
RUN_TIE_LATER,
|
||||
observations=(ResultObservation('alpha', 'hostname', 'later.example.test'),),
|
||||
)
|
||||
)
|
||||
)
|
||||
asyncio.run(
|
||||
store.save_run(
|
||||
_completed_run(
|
||||
RUN_ONE,
|
||||
observations=(ResultObservation('alpha', 'hostname', 'earlier.example.test'),),
|
||||
)
|
||||
)
|
||||
)
|
||||
asyncio.run(store.dispose())
|
||||
|
||||
assert (
|
||||
source_yields.main(
|
||||
['--database', str(database), '--run-id', str(RUN_TIE_LATER), '--changes', '--format', 'json']
|
||||
)
|
||||
== 0
|
||||
)
|
||||
|
||||
comparison = json.loads(capsys.readouterr().out)['comparisons'][0]
|
||||
assert comparison['baseline_run_id'] == str(RUN_ONE)
|
||||
|
||||
|
||||
def test_include_persisting_adds_unchanged_hostname_rows_without_changing_counts(
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
@@ -729,6 +766,9 @@ def test_unscoped_empty_database_reports_an_explicit_empty_scope(
|
||||
assert source_yields.main(['--database', str(database)]) == 0
|
||||
assert capsys.readouterr().out.splitlines()[:3] == ['Targets: none', 'Kind: hostname', 'Run count: 0']
|
||||
|
||||
assert source_yields.main(['--database', str(database), '--changes']) == 0
|
||||
assert capsys.readouterr().out.startswith('Targets: none\nComparison count: 0\n')
|
||||
|
||||
|
||||
def test_missing_database_fails_without_creating_file(
|
||||
tmp_path: Path,
|
||||
|
||||
@@ -1422,10 +1422,19 @@
|
||||
const query = event.target.value.trim().toLowerCase();
|
||||
state.resultTable?.setFilter(row => !query || row.value.toLowerCase().includes(query));
|
||||
});
|
||||
for (const filter of [
|
||||
nodes.trackingChange, nodes.trackingSource, nodes.trackingResolution,
|
||||
nodes.trackingExclusive, nodes.trackingPersisting
|
||||
]) filter.addEventListener('change', () => renderHostnameTracking(state.detail));
|
||||
nodes.trackingChange.addEventListener('change', () => {
|
||||
if (nodes.trackingChange.value === 'persisting') nodes.trackingPersisting.checked = true;
|
||||
renderHostnameTracking(state.detail);
|
||||
});
|
||||
nodes.trackingPersisting.addEventListener('change', () => {
|
||||
if (!nodes.trackingPersisting.checked && nodes.trackingChange.value === 'persisting') {
|
||||
nodes.trackingChange.value = '';
|
||||
}
|
||||
renderHostnameTracking(state.detail);
|
||||
});
|
||||
for (const filter of [nodes.trackingSource, nodes.trackingResolution, nodes.trackingExclusive]) {
|
||||
filter.addEventListener('change', () => renderHostnameTracking(state.detail));
|
||||
}
|
||||
nodes.sourceSearch.addEventListener('input', event => renderSourceGroups(event.target.value));
|
||||
nodes.selectCapability.addEventListener('click', selectCapability);
|
||||
nodes.selectP0.addEventListener('click', () => setP0Selection(true));
|
||||
|
||||
@@ -210,7 +210,8 @@ def _targets_table(rows: list[dict[str, str | int]]) -> str:
|
||||
def _tracking_table(payload: dict[str, object]) -> str:
|
||||
comparisons = cast('list[dict[str, object]]', payload['comparisons'])
|
||||
changes = cast('list[dict[str, object]]', payload['hostname_changes'])
|
||||
lines = [f'Target: {payload["target"]}', f'Comparison count: {payload["comparison_count"]}']
|
||||
scope = f'Target: {payload["target"]}' if payload['target'] is not None else 'Targets: none'
|
||||
lines = [scope, f'Comparison count: {payload["comparison_count"]}']
|
||||
summary_columns = (
|
||||
('run_id', 'CURRENT RUN'),
|
||||
('baseline_run_id', 'BASELINE RUN'),
|
||||
@@ -227,7 +228,7 @@ def _tracking_table(payload: dict[str, object]) -> str:
|
||||
}
|
||||
for comparison in comparisons
|
||||
]
|
||||
summary_widths = [max(len(label), *(len(str(row[key])) for row in summary_rows)) for key, label in summary_columns]
|
||||
summary_widths = [max([len(label), *(len(str(row[key])) for row in summary_rows)]) for key, label in summary_columns]
|
||||
lines.append(' '.join(label.ljust(summary_widths[index]) for index, (_key, label) in enumerate(summary_columns)))
|
||||
lines.extend(
|
||||
' '.join(str(row[key]).ljust(summary_widths[index]) for index, (key, _label) in enumerate(summary_columns))
|
||||
|
||||
Reference in New Issue
Block a user