StartAgenticValidation
Ad-hoc entry point for Tenzai exposure validation. On the first click it gathers exposure context (optionally enriching from the ASM service, or parsing the socket from the exposure name), creates a Tenzai validation scan (folding in the Cortex issue Description), and marks the issue Running. It then polls (via ScheduledCommand) until the scan is terminal and writes the verdict — status, exploit validation, assessment details, reproduction, fix guidance, credit usage, reference URL — back onto the issue.
python · Tenzai
Details
| ID | StartAgenticValidation |
|---|---|
| Language | python |
| From Version | 6.10.0 |
| Docker Image | demisto/python3:3.12.14.13053055 |
| Tags | incident-action-button |
README
Ad-hoc entry point for Tenzai exposure validation, invoked from the Start Tenzai Validation button on the Tenzai issue layout (the Validate tab).
On the first click it gathers the exposure context (optionally enriching from the ASM service via asm-get-external-service, or parsing the host:port from the exposure name), triggers a Tenzai validation assessment with tenzai-trigger-validation-check, and sets Tenzai Assessment Status to Running. It then drives the whole loop itself: a self-scheduling ScheduledCommand polls tenzai-fetch-validation-check until the assessment reaches a terminal status, then writes the verdict — status, exploit validation, assessment details, reproduction, fix guidance, credit usage, and reference URL — back onto the issue. It does not change the issue severity.
Transient fetch failures are retried on the next poll; if the polling window (MAX_POLLS) is exhausted without a verdict the status is set to Error rather than left on Running. A second click while a validation is already Running on the issue is a no-op (no duplicate assessment).
When run from the Tenzai Agentic Issue Validation playbook, the playbook passes poll=false: the script then only triggers and marks the issue Running, and the playbook owns the poll + write-back so the loop is not run twice.
Inputs
| Argument | Description |
|---|---|
| service_id | ASM ExternalService id; used to enrich the target when target is not provided. |
| target | The exposure target (IP or FQDN). If omitted, derived from the ASM service or parsed from exposure_name. |
| exposure_name | Human-readable exposure name (typically the issue name). |
| supporting_data | Free-text context (CVEs, rule, classification). |
| alert_internal_id | The Cortex issue/alert id for result correlation. |
| application_type / port / protocol / service_classification | Optional raw fields forwarded to Tenzai. |
| poll | Whether the script polls to completion and writes the verdict itself. Defaults to true (the button); the playbook passes false. |
Outputs
| Path | Description |
|---|---|
| Tenzai.ValidationCheck.checkId | The Tenzai validation assessment id. |
| Tenzai.ValidationCheck.status | The assessment status (Running while polling; Complete/Error when terminal). |
| Tenzai.ValidationCheck.validated | Whether the exposure was validated as exploitable (on terminal completion). |
| Tenzai.ValidationCheck.evidence | Markdown assessment details (on terminal completion). |
| Tenzai.ValidationCheck.reproduction | Markdown reproduction steps (on terminal completion). |
| Tenzai.ValidationCheck.guidance | Remediation or mitigation guidance (on terminal completion). |
| Tenzai.ValidationCheck.creditUsage | Approximate Tenzai credit/ACU cost of the assessment (on terminal completion). |
| Tenzai.ValidationCheck.referenceUrl | Deep link to the assessment results in Tenzai (on terminal completion). |
"""Unit tests for the StartAgenticValidation script.""" import demistomock as demisto import pytest from CommonServerPython import DemistoException from StartAgenticValidation import ( MAX_POLLS, TERMINAL_MAX_RETRIES, _findings_grid_rows, _verdict_is_enriched, extract_context, extract_context_list, start_agentic_validation, target_from_exposure_name, ) # ScheduledCommand construction requires a platform version that supports it. SUPPORTED_VERSION = {"version": "6.10.0", "buildNumber": "12345"} # tenzai-create-scan returns the scan under Tenzai.Scan with an `id`. CREATE_ENTRY = [{"Type": 1, "EntryContext": {"Tenzai.Scan(val.id == obj.id)": {"id": "scan_1", "status": "Pending"}}}] ENRICH_ENTRY = [ { "Type": 1, "EntryContext": { "ASM.ExternalService(val.service_id == obj.service_id)": { "service_id": "svc-1", "domain": "vpn.acme.com", "ip_address": "1.2.3.4", "port": 443, "protocol": "tcp", "active_classifications": ["WebServer"], "externally_inferred_cves": ["CVE-2024-1234"], } }, } ] def _result_entry(payload: dict, findings: list | None = None) -> list: """A tenzai-get-scan-result response entry. ``tenzai-get-scan-result`` returns the scan object under ``Tenzai.Scan`` and, when complete, the findings under their own top-level ``Tenzai.Finding`` context. """ context: dict = {"Tenzai.Scan(val.id == obj.id)": payload} if findings is not None: context["Tenzai.Finding(val.title == obj.title)"] = findings return [{"Type": 1, "EntryContext": context}] @pytest.fixture def first_run(mocker): """Platform supports scheduling and the issue has no in-flight validation.""" mocker.patch.object(demisto, "demistoVersion", return_value=SUPPORTED_VERSION) mocker.patch.object(demisto, "incident", create=True, return_value={"CustomFields": {}}) def test_findings_grid_rows_maps_and_drops_empty(): """Findings map to grid rows by column key; empty cells and empty findings are dropped.""" rows = _findings_grid_rows( [ {"title": "A", "severity": "HIGH", "detail": "combined markdown"}, {"title": "B", "severity": "LOW"}, # sparse — keeps title/severity only {}, # nothing usable — skipped entirely ] ) assert rows == [ {"finding": "A", "severity": "HIGH", "detail": "combined markdown"}, {"finding": "B", "severity": "LOW"}, ] assert _findings_grid_rows([]) == [] assert _findings_grid_rows(None) == [] # type: ignore[arg-type] def test_findings_grid_rows_carries_cve_and_attribution(): """A non-own finding carries its CVE and attribution into the grid row; 'own' is dropped (default).""" rows = _findings_grid_rows( [ {"title": "own", "severity": "HIGH", "cve": "CVE-2023-44487", "attribution": "own"}, {"title": "byproduct", "severity": "HIGH", "cve": "CVE-2024-23897", "attribution": "discovered"}, {"title": "uncorrelated", "severity": "LOW", "cve": "CVE-2020-0001", "attribution": "unattributed"}, ] ) assert rows[0] == {"finding": "own", "severity": "HIGH", "cve": "CVE-2023-44487"} # own dropped assert rows[1] == { "finding": "byproduct", "severity": "HIGH", "cve": "CVE-2024-23897", "attribution": "discovered", } assert rows[2]["attribution"] == "unattributed" def test_extract_context_strips_dt_selector(): """The DT selector in the context key is ignored when matching the prefix.""" assert extract_context(CREATE_ENTRY, "Tenzai.Scan") == {"id": "scan_1", "status": "Pending"} assert extract_context([], "Tenzai.Scan") == {} def test_extract_context_list_returns_findings(): """Findings are read from their own top-level Tenzai.Finding context as a list.""" entries = _result_entry( {"id": "scan_1", "status": "Complete", "validated": True}, findings=[{"title": "A", "severity": "HIGH"}, {"title": "B", "severity": "LOW"}], ) findings = extract_context_list(entries, "Tenzai.Finding") assert [f["title"] for f in findings] == ["A", "B"] # No findings context -> empty list (not an error). assert extract_context_list(_result_entry({"id": "s", "status": "Running"}), "Tenzai.Finding") == [] @pytest.mark.parametrize( "name,expected", [ ("PPTP Server at 1.2.3.4:1723", "1.2.3.4:1723"), ("Unencrypted FTP Server at 10.0.0.5:21", "10.0.0.5:21"), ("Insecure NGINX Web Server (1.17.7) at 10.0.0.5:80", "10.0.0.5:80"), ("Exposed VPN at vpn.acme.com:443", "vpn.acme.com:443"), ("Bare host at 10.0.0.5", "10.0.0.5"), ("No socket in this name", None), ("", None), ], ) def test_target_from_exposure_name(name, expected): """The host[:port] is recovered from the ASM exposure-name pattern.""" assert target_from_exposure_name(name) == expected def test_button_creates_and_polls(mocker, first_run): """First click (button): creates a scan, marks Running, and starts the self-poll loop.""" calls = [] def fake_execute(command, args): calls.append((command, args)) if command == "tenzai-create-scan": return CREATE_ENTRY return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) result = start_agentic_validation({"target": "1.2.3.4", "exposure_name": "Exposed RDP", "alert_internal_id": "a-9"}) commands = [c[0] for c in calls] assert "tenzai-create-scan" in commands assert ("setIncident", {"tenzaiassessmentstatus": "Running"}) in calls # The correlation keys ride on the outputs (here just alert_id, from alert_internal_id) so the # verdict fetch can scope to this alert's lead. assert result.outputs == {"id": "scan_1", "status": "Running", "alert_id": "a-9"} assert result.scheduled_command is not None # self-poll loop started def test_button_writes_live_reference_url_on_running(mocker, first_run): """When create returns a live Agent-log URL, it is written to the issue at Running time (ENG-5200).""" log_url = "https://app.tenzai.io/apps/a/tests/t/log" calls = [] def fake_execute(command, args): calls.append((command, args)) if command == "tenzai-create-scan": return [ { "Type": 1, "EntryContext": { "Tenzai.Scan(val.id == obj.id)": {"id": "scan_1", "status": "Pending", "referenceUrl": log_url} }, } ] return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) result = start_agentic_validation({"target": "1.2.3.4", "exposure_name": "Exposed RDP"}) # The Running write carries the live Agent-log link (verdict outputs are unchanged). assert ("setIncident", {"tenzaiassessmentstatus": "Running", "tenzaireferenceurl": log_url}) in calls assert result.outputs == {"id": "scan_1", "status": "Running"} def test_button_forwards_analyst_guidelines(mocker, first_run): """Analyst guidelines from the Actions section ride along to tenzai-create-scan (ENG-6970).""" calls = [] def fake_execute(command, args): calls.append((command, args)) if command == "tenzai-create-scan": return CREATE_ENTRY return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) start_agentic_validation({"target": "1.2.3.4", "exposure_name": "Exposed RDP", "guidelines": "Focus on the login form."}) create_args = next(args for command, args in calls if command == "tenzai-create-scan") assert create_args["guidelines"] == "Focus on the login form." def test_button_omits_guidelines_when_blank(mocker, first_run): """No analyst guidelines => the arg is not sent at all (assign_params drops empties).""" calls = [] def fake_execute(command, args): calls.append((command, args)) if command == "tenzai-create-scan": return CREATE_ENTRY return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) start_agentic_validation({"target": "1.2.3.4", "exposure_name": "Exposed RDP"}) create_args = next(args for command, args in calls if command == "tenzai-create-scan") assert "guidelines" not in create_args def test_button_dedupes_when_already_running(mocker): """A second click while a validation is already Running does not start another scan.""" mocker.patch.object(demisto, "demistoVersion", return_value=SUPPORTED_VERSION) mocker.patch.object(demisto, "incident", create=True, return_value={"CustomFields": {"tenzaiassessmentstatus": "Running"}}) calls = [] mocker.patch.object(demisto, "executeCommand", side_effect=lambda c, a: calls.append((c, a)) or [{"Type": 1}]) result = start_agentic_validation({"target": "1.2.3.4", "exposure_name": "Exposed RDP"}) assert "tenzai-create-scan" not in [c[0] for c in calls] assert result.scheduled_command is None assert "already running" in result.readable_output.lower() def test_playbook_path_creates_only(mocker, first_run): """With poll=false (the playbook), the script only creates the scan — no self-poll loop.""" calls = [] def fake_execute(command, args): calls.append((command, args)) if command == "tenzai-create-scan": return CREATE_ENTRY return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) result = start_agentic_validation( {"poll": "false", "target": "1.2.3.4", "exposure_name": "Exposed RDP", "alert_internal_id": "a-1"} ) assert "tenzai-create-scan" in [c[0] for c in calls] assert ("setIncident", {"tenzaiassessmentstatus": "Running"}) in calls # The playbook path surfaces the correlation keys (alert_id here) so task 3 can pass them on. assert result.outputs == {"id": "scan_1", "status": "Running", "alert_id": "a-1"} assert result.scheduled_command is None # the playbook owns polling/write-back def test_enriches_from_asm_service_and_passes_issue_description(mocker, first_run): """With only a service_id, the script enriches from ASM and passes raw fields + the issue Description to tenzai-create-scan.""" captured = {} def fake_execute(command, args): if command == "asm-get-external-service": return ENRICH_ENTRY if command == "tenzai-create-scan": captured["create_args"] = args return CREATE_ENTRY return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) start_agentic_validation( { "service_id": "svc-1", "exposure_name": "Insecure OpenSSH", "issue_description": "CVE-2018-15473 user enumeration on the SSH server", "alert_internal_id": "a-1", } ) ca = captured["create_args"] assert ca["target"] == "vpn.acme.com" # domain preferred over ip assert ca["port"] == "443" assert ca["protocol"] == "tcp" assert ca["service_classification"] == "WebServer" assert "CVE-2024-1234" in ca["supporting_data"] # service-wide CVEs ride as evidence assert ca["asm_service_id"] == "svc-1" assert ca["issue_description"] == "CVE-2018-15473 user enumeration on the SSH server" # The service's inferred CVEs are context only — NOT promoted to cve_id/category, # since they belong to the IP/service, not this specific issue. assert "cve_id" not in ca def test_falls_back_to_socket_in_exposure_name(mocker, first_run): """With no target and no (resolvable) service_id, parse host:port from the name and create.""" commands = [] captured = {} def fake_execute(command, args): commands.append(command) if command == "tenzai-create-scan": captured["create_args"] = args return CREATE_ENTRY return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) start_agentic_validation({"exposure_name": "PPTP Server at 1.2.3.4:1723", "alert_internal_id": "511620"}) assert "asm-get-external-service" not in commands assert captured["create_args"]["target"] == "1.2.3.4:1723" def test_without_target_or_service_raises(mocker, first_run): """No target, no resolvable service id, and no parseable name is an error.""" mocker.patch.object(demisto, "executeCommand", side_effect=lambda c, a: [{"Type": 1}]) with pytest.raises(DemistoException): start_agentic_validation({"exposure_name": "x"}) def test_poll_still_running_reschedules(mocker): """A re-entry that finds the scan still running reschedules and writes nothing.""" mocker.patch.object(demisto, "demistoVersion", return_value=SUPPORTED_VERSION) set_incident_calls = [] fetch_args = {} def fake_execute(command, args): if command == "tenzai-get-scan-result": fetch_args.update(args) return _result_entry({"id": "scan_1", "status": "Running", "validated": False}) if command == "setIncident": set_incident_calls.append(args) return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) result = start_agentic_validation({"scan_id_internal": "scan_1", "poll_count": "1"}) assert result.scheduled_command is not None assert set_incident_calls == [] # nothing written while running assert fetch_args == {"id": "scan_1"} # the integration expects `id`, not scan_id def test_poll_complete_writes_verdict(mocker): """A terminal re-entry writes the full verdict and stops polling.""" mocker.patch.object(demisto, "demistoVersion", return_value=SUPPORTED_VERSION) set_incident_calls = [] def fake_execute(command, args): if command == "tenzai-get-scan-result": return _result_entry( { "id": "scan_1", "status": "Complete", "validated": True, "correlationState": "resolved", "exposureStatus": "MATERIALIZED", "evidence": "## Confirmed exploitable", "reproduction": "1. Connect\n2. Exploit", "guidance": "Upgrade to the patched version.", "creditUsage": 12, "duration": 252, "startedAt": "2026-08-17T19:13:46.192631Z", "referenceUrl": "https://app.tenzai.io/apps/a/tests/t/findings", }, findings=[ { "title": "PPTP cleartext auth", "severity": "HIGH", "detail": "## Impact\nCredentials exposed.\n\n## Reproduction\n\n1. Connect", }, {"title": "SMB signing not required", "severity": "MEDIUM"}, ], ) if command == "setIncident": set_incident_calls.append(args) return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) result = start_agentic_validation({"scan_id_internal": "scan_1", "poll_count": "2"}) assert result.scheduled_command is None fields = set_incident_calls[0] assert fields["tenzaiassessmentstatus"] == "Complete" assert fields["tenzaiissuevalidated"] == "true" assert fields["tenzaireproduction"] == "1. Connect\n2. Exploit" assert fields["tenzaicreditusage"] == 12 assert fields["tenzaiassessmentduration"] == 252 assert fields["tenzaistartedat"] == "2026-08-17T19:13:46.192631Z" # feeds the panel "Started at" cell assert fields["tenzaireferenceurl"].endswith("/findings") assert "severity" not in fields # Tenzai records the verdict but does not change severity # The findings populate the Tenzai Findings grid via a customFields setIncident call. grid_call = next(args for args in set_incident_calls if "customFields" in args) rows = grid_call["customFields"]["tenzaifindings"] assert len(rows) == 2 assert rows[0] == { "finding": "PPTP cleartext auth", "severity": "HIGH", "detail": "## Impact\nCredentials exposed.\n\n## Reproduction\n\n1. Connect", } # A sparse finding still renders its title/severity; empty cells are dropped. assert rows[1] == {"finding": "SMB signing not required", "severity": "MEDIUM"} def test_poll_complete_not_validated(mocker): """A completed-but-not-reproducible verdict records false and leaves severity untouched.""" mocker.patch.object(demisto, "demistoVersion", return_value=SUPPORTED_VERSION) set_incident_calls = [] def fake_execute(command, args): if command == "tenzai-get-scan-result": return _result_entry( { "id": "s", "status": "Complete", "validated": False, "correlationState": "resolved", "exposureStatus": "INVALIDATED", "evidence": "No confirmed findings.", } ) if command == "setIncident": set_incident_calls.append(args) return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) start_agentic_validation({"scan_id_internal": "s", "poll_count": "2"}) fields = set_incident_calls[0] assert fields["tenzaiissuevalidated"] == "false" assert "severity" not in fields # Tenzai does not lower severity # No duration on this result => the numeric field is simply not written (not coerced to ""). assert "tenzaiassessmentduration" not in fields # The grid is overwritten even with no findings, so a prior run's rows can't # linger under a not-exploitable verdict (ENG-5198). grid_call = next(args for args in set_incident_calls if "customFields" in args) assert grid_call["customFields"]["tenzaifindings"] == [] def test_poll_complete_clears_stale_findings_grid(mocker): """A 0-finding terminal verdict overwrites the findings grid with an empty list. Regression guard for ENG-5198: the write-back must set the grid on *every* terminal write (not only when findings exist), so a fresh not-exploitable verdict overwrites — rather than inherits — a prior scan's findings rows. """ mocker.patch.object(demisto, "demistoVersion", return_value=SUPPORTED_VERSION) set_incident_calls = [] def fake_execute(command, args): if command == "tenzai-get-scan-result": # SUCCESS with no findings -> Not Exploitable. Note: no Tenzai.Finding context. return _result_entry( { "id": "s", "status": "Complete", "validated": False, "correlationState": "resolved", "exposureStatus": "INVALIDATED", "evidence": "No exploitable findings were confirmed for this exposure.", "creditUsage": 3.04, "referenceUrl": "https://app.tenzai.io/apps/a/tests/t/findings", }, findings=[], ) if command == "setIncident": set_incident_calls.append(args) return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) start_agentic_validation({"scan_id_internal": "s", "poll_count": "2"}) # Exactly one grid write, carrying empty lists (clears any stale rows) for all grid fields. grid_calls = [args for args in set_incident_calls if "customFields" in args] assert len(grid_calls) == 1 assert grid_calls[0] == {"customFields": {"tenzaifindings": [], "tenzaitimeline": []}} # The scalar result block is written atomically: fresh evidence, no stale # reproduction/guidance carried over from a would-be prior run. scalar = set_incident_calls[0] assert scalar["tenzaiissuevalidated"] == "false" assert scalar["tenzaiassessmentevidence"].startswith("No exploitable findings") assert scalar["tenzaireproduction"] == "" assert scalar["tenzaiguidance"] == "" assert scalar["tenzaiissuerationale"] == "" # cleared when the scan has no CVE rationale def test_poll_complete_writes_cve_rationale(mocker): """A CVE lead rationale on the result is persisted to tenzaiissuerationale (ENG-4910).""" mocker.patch.object(demisto, "demistoVersion", return_value=SUPPORTED_VERSION) rationale = "## Description\n\nCVE hypothesis.\n\n## Conclusion\n\nCONFIRMED FALSE POSITIVE." set_incident_calls = [] def fake_execute(command, args): if command == "tenzai-get-scan-result": return _result_entry( { "id": "s", "status": "Complete", "validated": False, "correlationState": "resolved", "exposureStatus": "INVALIDATED", "evidence": "x", "leadRationale": rationale, } ) if command == "setIncident": set_incident_calls.append(args) return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) start_agentic_validation({"scan_id_internal": "s", "poll_count": "2"}) assert set_incident_calls[0]["tenzaiissuerationale"] == rationale def test_poll_non_bool_validated_clears_rather_than_escalates(mocker): """A non-boolean 'validated' (only a real True/False is a verdict) clears the field, never escalates. Only a Python bool True/False maps to "true"/"false"; anything else (a stray string, None) is inconclusive and clears tenzaiissuevalidated to "" so a prior run's "true" cannot survive. """ mocker.patch.object(demisto, "demistoVersion", return_value=SUPPORTED_VERSION) set_incident_calls = [] def fake_execute(command, args): if command == "tenzai-get-scan-result": return _result_entry( {"id": "s", "status": "Complete", "validated": "false", "correlationState": "unmatched", "evidence": "x"} ) if command == "setIncident": set_incident_calls.append(args) return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) start_agentic_validation({"scan_id_internal": "s", "poll_count": "2"}) fields = set_incident_calls[0] assert "severity" not in fields # not escalated on a truthy non-bool assert fields["tenzaiissuevalidated"] == "" # cleared, not left to drift from a prior run def test_poll_inconclusive_clears_stale_verdict(mocker): """A None (BLOCKED/unmatched) verdict clears tenzaiissuevalidated so a prior 'true' cannot survive.""" mocker.patch.object(demisto, "demistoVersion", return_value=SUPPORTED_VERSION) set_incident_calls = [] def fake_execute(command, args): if command == "tenzai-get-scan-result": return _result_entry( { "id": "s", "status": "Complete", "validated": None, "correlationState": "resolved", "evidence": "x", "exposureStatus": "BLOCKED", } ) if command == "setIncident": set_incident_calls.append(args) return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) start_agentic_validation({"scan_id_internal": "s", "poll_count": "2"}) assert set_incident_calls[0]["tenzaiissuevalidated"] == "" def test_write_only_playbook_path_writes_grid_and_timeline(mocker): """The playbook (write_only) re-entry fetches once and persists the full verdict — scalars, the findings grid, and the timeline — through the SAME writer the button uses, with no rescheduling.""" mocker.patch.object(demisto, "demistoVersion", return_value=SUPPORTED_VERSION) set_incident_calls = [] fetch_args = {} def fake_execute(command, args): if command == "tenzai-get-scan-result": fetch_args.update(args) return _result_entry( { "id": "scan_1", "status": "Complete", "validated": False, "correlationState": "resolved", "evidence": "No exploitable findings were confirmed for this exposure.", "exposureStatus": "INVALIDATED", "leadRationale": "## Conclusion\n\nCONFIRMED FALSE POSITIVE.", "timeline": [{"status": "OPEN", "time": "08-20 15:36"}], }, findings=[ { "title": "CVE-2024-23897: Arbitrary File Read", "severity": "HIGH", "detail": "x", "cve": "CVE-2024-23897", "attribution": "discovered", }, ], ) if command == "setIncident": set_incident_calls.append(args) return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) result = start_agentic_validation( {"scan_id_internal": "scan_1", "write_only": "true", "alert_id": "600052", "cve": "CVE-2023-44487"} ) # Fetched once, scoped by the correlation keys; no reschedule. assert fetch_args == {"id": "scan_1", "alert_id": "600052", "cve": "CVE-2023-44487"} assert result.scheduled_command is None # The grid + timeline are written on the playbook path (the bug this fixes). grid_call = next(args for args in set_incident_calls if "customFields" in args) rows = grid_call["customFields"]["tenzaifindings"] assert rows[0]["attribution"] == "discovered" assert grid_call["customFields"]["tenzaitimeline"] == [{"status": "OPEN", "time": "08-20 15:36"}] # And the verdict is cleared (INVALIDATED -> validated False here, written explicitly). scalar = set_incident_calls[0] assert scalar["tenzaiissuevalidated"] == "false" def test_poll_re_supplies_correlation_keys_to_result_fetch(mocker): """The scheduled re-entry re-supplies alert_id/cve/rule_id to tenzai-get-scan-result and forwards them.""" mocker.patch.object(demisto, "demistoVersion", return_value=SUPPORTED_VERSION) fetch_args = {} def fake_execute(command, args): if command == "tenzai-get-scan-result": fetch_args.update(args) return _result_entry({"id": "scan_1", "status": "Running", "validated": None}) return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) result = start_agentic_validation( { "scan_id_internal": "scan_1", "poll_count": "1", "alert_id": "600052", "cve": "CVE-2023-44487", "rule_id": "rule-9", } ) # The verdict fetch is scoped by the carried correlation keys... assert fetch_args == { "id": "scan_1", "alert_id": "600052", "cve": "CVE-2023-44487", "rule_id": "rule-9", } # ...and the keys ride onto the next scheduled tick. next_args = result.scheduled_command._args assert next_args["alert_id"] == "600052" assert next_args["cve"] == "CVE-2023-44487" assert next_args["rule_id"] == "rule-9" def test_poll_timeout_marks_error(mocker): """When the polling window is exhausted with no verdict, the status is set to Error (not stuck Running).""" mocker.patch.object(demisto, "demistoVersion", return_value=SUPPORTED_VERSION) set_incident_calls = [] def fake_execute(command, args): if command == "tenzai-get-scan-result": return _result_entry({"id": "scan_1", "status": "Running"}) if command == "setIncident": set_incident_calls.append(args) return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) result = start_agentic_validation({"scan_id_internal": "scan_1", "poll_count": str(MAX_POLLS)}) assert result.scheduled_command is None # stop polling assert set_incident_calls == [{"tenzaiassessmentstatus": "Error"}] def test_poll_transient_fetch_error_reschedules(mocker): """A transient fetch error retries on the next tick rather than aborting the loop.""" mocker.patch.object(demisto, "demistoVersion", return_value=SUPPORTED_VERSION) mocker.patch.object(demisto, "error") # the retry path logs via demisto.error; keep it off stdout in tests set_incident_calls = [] def fake_execute(command, args): if command == "tenzai-get-scan-result": return [{"Type": 4, "Contents": "transient 503"}] # error entry → run_command raises if command == "setIncident": set_incident_calls.append(args) return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) result = start_agentic_validation({"scan_id_internal": "scan_1", "poll_count": "1"}) assert result.scheduled_command is not None # retried, not aborted assert set_incident_calls == [] # no premature verdict/error write # --------------------------------------------------------------------------- # Enrichment-readiness gate (ENG-7505) # --------------------------------------------------------------------------- def test_verdict_is_enriched_open_status_not_ready(): """A non-empty but still-open lead status (OPEN / IN_PROGRESS) is NOT enriched — the lead has not closed yet, so requiring merely-non-empty status would write prematurely.""" assert _verdict_is_enriched({"exposureStatus": "IN_PROGRESS", "leadRationale": "x"}, cve_supplied=True) is False assert _verdict_is_enriched({"exposureStatus": "OPEN"}, cve_supplied=False) is False def test_verdict_is_enriched_terminal_status_ready(): assert _verdict_is_enriched({"exposureStatus": "INVALIDATED", "leadRationale": "r"}, cve_supplied=True) is True assert _verdict_is_enriched({"exposureStatus": "MATERIALIZED"}, cve_supplied=False) is True def test_verdict_is_enriched_requires_rationale_for_concluded_cve(): unenriched = {"exposureStatus": "INVALIDATED", "leadRationale": ""} assert _verdict_is_enriched(unenriched, cve_supplied=True) is False enriched = {"exposureStatus": "INVALIDATED", "leadRationale": "FALSE POSITIVE — refuted."} assert _verdict_is_enriched(enriched, cve_supplied=True) is True def test_verdict_is_enriched_blocked_needs_no_rationale(): """BLOCKED (reached but stopped at the safety boundary) has no conclusion, so a CVE lead is ready without a rationale — otherwise the loop would spin to the retry cap every time.""" assert _verdict_is_enriched({"exposureStatus": "BLOCKED", "leadRationale": ""}, cve_supplied=True) is True def test_verdict_is_enriched_zero_findings_not_missing(): """INVALIDATED legitimately has zero findings — the findings count never gates readiness.""" result = {"exposureStatus": "INVALIDATED", "leadRationale": "refuted", "creditUsage": None, "startedAt": None} assert _verdict_is_enriched(result, cve_supplied=True) is True def _run_reentry(mocker, payload, args, findings=None): """Drive a scheduled re-entry, capturing setIncident calls. When ``payload`` is None the tenzai-get-scan-result fetch raises (transient failure).""" mocker.patch.object(demisto, "demistoVersion", return_value=SUPPORTED_VERSION) mocker.patch.object(demisto, "error") # retry/exhaustion paths log via demisto.error; keep off stdout mocker.patch.object(demisto, "debug") # the retry-cap path logs via demisto.debug calls: list = [] def fake_execute(command, a): if command == "tenzai-get-scan-result": if payload is None: return [{"Type": 4, "Contents": "transient 503"}] # error entry -> run_command raises return _result_entry(payload, findings=findings) if command == "setIncident": calls.append(a) return [{"Type": 1, "Contents": "ok"}] mocker.patch.object(demisto, "executeCommand", side_effect=fake_execute) return start_agentic_validation(args), calls def test_poll_resolved_unenriched_reschedules_without_clearing(mocker): """Complete + a matched lead whose enrichment hasn't landed → reschedule, write NOTHING (no half-cleared card), carrying an incremented terminal_retry_count.""" payload = { "id": "scan_1", "status": "Complete", "validated": False, "correlationState": "resolved", "exposureStatus": "", "leadRationale": "", } result, calls = _run_reentry(mocker, payload, {"scan_id_internal": "scan_1", "poll_count": "3", "cve": "CVE-2023-44487"}) assert result.scheduled_command is not None assert calls == [] assert result.scheduled_command._args.get("terminal_retry_count") == 1 def test_poll_resolved_open_status_reschedules(mocker): """Terminal scan but the matched lead is still IN_PROGRESS → wait, don't write.""" payload = { "id": "scan_1", "status": "Complete", "validated": None, "correlationState": "resolved", "exposureStatus": "IN_PROGRESS", } result, calls = _run_reentry(mocker, payload, {"scan_id_internal": "scan_1", "poll_count": "3"}) assert result.scheduled_command is not None assert calls == [] def test_poll_pending_reschedules_no_partial_write(mocker): """correlationState=pending (failed/empty leads fetch) must keep waiting, never write a partial verdict — this is the original bug the tri-state guards against.""" payload = {"id": "scan_1", "status": "Complete", "validated": None, "correlationState": "pending"} result, calls = _run_reentry(mocker, payload, {"scan_id_internal": "scan_1", "poll_count": "3"}) assert result.scheduled_command is not None assert calls == [] def test_poll_unmatched_writes_immediately(mocker): """Leads fetched, none correlate (unmatched) → definitive; write the inconclusive verdict now.""" payload = {"id": "scan_1", "status": "Complete", "validated": None, "correlationState": "unmatched"} result, calls = _run_reentry(mocker, payload, {"scan_id_internal": "scan_1", "poll_count": "3"}) assert result.scheduled_command is None assert calls assert calls[0]["tenzaiassessmentstatus"] == "Complete" assert calls[0]["tenzaiissuevalidated"] == "" # inconclusive -> cleared def test_poll_resolved_enriched_writes_invalidated(mocker): """A later enriched tick writes the full verdict: INVALIDATED + start time + ACU + rationale.""" payload = { "id": "scan_1", "status": "Complete", "validated": False, "correlationState": "resolved", "exposureStatus": "INVALIDATED", "leadRationale": "FALSE POSITIVE — refuted.", "creditUsage": 2.18, "startedAt": "2026-08-31T12:00:00Z", } result, calls = _run_reentry( mocker, payload, {"scan_id_internal": "scan_1", "poll_count": "3", "terminal_retry_count": "1", "cve": "CVE-2023-44487"}, ) assert result.scheduled_command is None fields = calls[0] assert fields["tenzaiissuevalidated"] == "false" assert fields["tenzaiexposurestatus"] == "INVALIDATED" assert fields["tenzaistartedat"] == "2026-08-31T12:00:00Z" assert fields["tenzaicreditusage"] == 2.18 assert fields["tenzaiissuerationale"] == "FALSE POSITIVE — refuted." def test_poll_retry_cap_writes_partial(mocker): """Enrichment window exhausted on a terminal result → write the partial verdict, stop polling.""" payload = { "id": "scan_1", "status": "Complete", "validated": False, "correlationState": "resolved", "exposureStatus": "", "leadRationale": "", } result, calls = _run_reentry( mocker, payload, { "scan_id_internal": "scan_1", "poll_count": "3", "terminal_retry_count": str(TERMINAL_MAX_RETRIES), "cve": "CVE-2023-44487", }, ) assert result.scheduled_command is None assert calls assert "tenzaiassessmentstatus" in calls[0] def test_write_only_reschedules_when_unenriched(mocker): """write_only must ALSO retry: the playbook polls scan status, not enrichment readiness.""" payload = { "id": "scan_1", "status": "Complete", "validated": False, "correlationState": "resolved", "exposureStatus": "", "leadRationale": "", } result, calls = _run_reentry(mocker, payload, {"scan_id_internal": "scan_1", "write_only": "true", "cve": "CVE-2023-44487"}) assert result.scheduled_command is not None assert result.scheduled_command._args.get("write_only") == "true" assert result.scheduled_command._args.get("terminal_retry_count") == 1 assert calls == [] def test_write_only_writes_when_enriched(mocker): """write_only + a fully-enriched MATERIALIZED result writes Exploitable + attributed findings.""" payload = { "id": "scan_1", "status": "Complete", "validated": True, "correlationState": "resolved", "exposureStatus": "MATERIALIZED", "leadRationale": "Confirmed exploit.", "creditUsage": 5, } result, calls = _run_reentry( mocker, payload, {"scan_id_internal": "scan_1", "write_only": "true", "cve": "CVE-2024-23897"}, findings=[{"title": "Arbitrary file read", "severity": "CRITICAL"}], ) assert result.scheduled_command is None assert calls[0]["tenzaiissuevalidated"] == "true" assert calls[0]["tenzaiexposurestatus"] == "MATERIALIZED" grid = next(a for a in calls if "customFields" in a) assert len(grid["customFields"]["tenzaifindings"]) == 1 def test_write_only_marks_error_on_fetch_failure_cap(mocker): """write_only that never gets a terminal result (repeated fetch failures) must mark the issue Error at the cap, not leave it stuck on Running forever.""" result, calls = _run_reentry( mocker, None, {"scan_id_internal": "scan_1", "write_only": "true", "terminal_retry_count": str(TERMINAL_MAX_RETRIES)}, ) assert result.scheduled_command is None assert calls assert calls[0]["tenzaiassessmentstatus"] == "Error"