Webhook icon
Script icon
Docker icon
If icon
Parallel icon
Sequential icon
SetVariables icon
Log icon
SlackIncomingWebhook icon
Pause icon
Switch icon
Create icon
Fail icon

Multi-Source URL Investigation and Analyst Approval

Query URLhaus and optional OTX, persist the risk report, then pause for an analyst before any GitHub issue.

Categories
Infrastructure

Normalize a submitted or defanged URL, query URLhaus and optional AlienVault OTX, and write an explainable risk report before any human decision. A MATCH is evidence from that source, not proof the URL is active on a host right now. NO_MATCH means that source had no record. It is not a determination that the URL is safe, and this workflow does not claim to detect every malware sample.

How it works

  1. A webhook body url, or the target_url input, is refanged (hxxp, hxxps, [.], (.)). Only http and https URLs with a host pass. Userinfo, including an empty username such as http://@example.com/, is rejected and redacted. Malformed hosts that cannot be parsed are rejected the same way. The flow never requests the submitted URL.
  2. query_sources runs URLhaus and optional AlienVault OTX in parallel with onChildFailure: CONTINUE. Each child copies its completed state and result into execution variables, so the report task does not read a nested task that never ran. Statuses stay MATCH, NO_MATCH, ERROR, RATE_LIMITED, SKIPPED_NO_CREDENTIALS, or SKIPPED. A failed, rate-limited, disabled, or missing result is not rewritten as NO_MATCH. One source failing keeps the other source's result.
  3. correlate_and_write_initial_report writes investigation_report.json with AWAITING_REVIEW before await_analyst_decision. The report includes source results, the risk rationale, analyst notes, and the effect of having no decision yet. The Pause defaults to REJECT, requires a reason, and uses behavior: CANCEL. A timeout leaves that pre-review report in place and does not approve the investigation.
  4. After resume, each branch records a GitHub outcome in execution variables before the audit task runs. record_analyst_decision reads those variables. It does not dereference create_security_ticket when that task was skipped. APPROVE may open an issue only when the recommended action is open_github_issue and github_repository is owner/repo. REJECT, ESCALATE, any other value, and a timeout do not authorize GitHub. The decision does not change overall_risk or the stored source results. Slack is optional, uses allowFailure: true, and runs only after the report file already exists. Turning Slack on does not prove a message was delivered.

Failure paths

  • Invalid URL: write_invalid_url_record stores INVALID_URL, then fail_invalid_url stops the run. No lookup and no Pause.
  • Empty URLHAUS_AUTH_KEY or OTX_API_KEY: that source is SKIPPED_NO_CREDENTIALS and no request is sent. The other source is kept.
  • HTTP 401 or 403: ERROR on the first attempt. It is not a no-match and it is not retried.
  • HTTP 429, HTTP 5xx, or a timeout: up to 3 attempts. The final status is RATE_LIMITED or ERROR. The other source's result stays in the report.
  • One child task failing: onChildFailure: CONTINUE so correlation still runs. analyst_notes in the JSON report says how to read each status. A task that crashes is recorded as ERROR, not as a clean or no-match result.
  • OTX disabled with enable_otx: false: that source is SKIPPED and OTX_API_KEY is not read. URLhaus is still queried.
  • Pause timeout: behavior: CANCEL. The pre-review report remains. The error task does not run, and nothing is approved. Resume is not implied by the clock running out.

Decision paths

  • Authorized APPROVE, with open_github_issue and a valid owner/repo, when GitHub returns a confirmed issue URL and number: investigation_report_final.json records issue_created.
  • APPROVE when the recommended action is not an issue, github_repository is blank, or the value is not owner/repo: the audit records not_configured. A full GitHub URL is not accepted. GitHub is not called on this path.
  • The GitHub task runs and fails, or it finishes without a confirmed issue URL: the audit records failed_or_unconfirmed. The report leaves the issue URL and number empty rather than claiming the issue was definitely not created.
  • REJECT and ESCALATE: the choice is logged and the audit records not_authorized. No GitHub issue is authorized.
  • Any other resumed value: log_invalid_decision runs, and the audit records INVALID_DECISION with action outcome not_authorized.
  • record_analyst_decision runs before notify_slack_decision. A Slack failure cannot remove investigation_report_final.json. The notification field is disabled or delivery_not_verified. It is not a delivery receipt.
flowchart TD
  startNode[Webhook or target_url] --> validate[Validate]
  validate -->|invalid| stopNode[Record and fail]
  validate -->|valid| parallel[URLhaus and OTX]
  parallel --> report[Write report]
  report --> pause[Pause]
  pause -->|timeout CANCEL| kept[Pre-review report remains]
  pause -->|APPROVE and owner/repo| issue[GitHub issue or unconfirmed]
  pause -->|REJECT or ESCALATE| audit[Audit only]

What you get

  • investigation_report.json before review, including source results, risk rationale, analyst_notes, and decision_effect.
  • investigation_report_final.json after a real decision. The original overall_risk, risk explanation, and source results are copied forward, then the analyst decision and action outcome are added.
  • Partial results when one source fails, is rate limited, is disabled, or has no credential. The other source remains in the report.
  • A named action outcome: issue_created, not_configured, failed_or_unconfirmed, or not_authorized. Logs explain which branch ran. They do not treat a missing issue URL as proof that GitHub created nothing.

Who it's for

SOC and incident-response teams that want a human decision before a security ticket. It is also for platform engineers who need the lookup, the approval, and the audit file in one execution instead of a script that both queries and files a ticket.

Why orchestrate this with Kestra

Parallel lookups, a durable report, a Pause gate, and a conditional GitHub task stay in one execution. Retries for HTTP 429 and server errors stay next to the source that failed. A skipped or failed source cannot wipe the other result. The same run records what failed, what was skipped, which decision was allowed to open an issue, and whether that issue result was confirmed. Secret values stay in Kestra secrets and are not written into the report as credentials.

Prerequisites

Kestra 2.0.7 can run this flow after the script, Docker task runner, GitHub, and Slack plugins are available. io.kestra:script:2.0.7 provides io.kestra.plugin.scripts.runner.docker.Docker. Python tasks use python:3.11-slim and install requests and kestra at runtime. Docker socket access is required. plugin-docker is a different plugin.

The GitHub Create task omits labels. GitHub returns HTTP 422 when a label is missing, and then no issue is created.

Secrets

  • URLHAUS_AUTH_KEY: URLhaus Auth-Key. An empty value skips URLhaus.
  • OTX_API_KEY: read only when enable_otx is true. An empty value skips OTX.
  • URLHAUS_WEBHOOK_KEY: webhook path secret. The body can send url only.
  • GITHUB_TOKEN: read only on the approved issue branch.
  • SLACK_WEBHOOK_URL: read only when a Slack task runs. Slack cannot submit the decision.

Leave github_repository blank and enable_slack_notification false for a run that must not open an issue or send Slack.

Quick start

  1. Store the URLhaus, OTX, and webhook secrets. Add the GitHub token only when an approved run should open an issue.
  2. Import this flow into company.security and execute an http or https URL.
  3. Download investigation_report.json from correlate_and_write_initial_report before resuming. Confirm AWAITING_REVIEW.
  4. Resume with REJECT or ESCALATE and a reason. Resume with APPROVE only for a repository you control, and only after you have read the pre-review report.
  5. Download investigation_report_final.json and confirm the action outcome. issue_created should include an issue URL. Every other outcome should leave that URL empty.

How to extend

  • Add another read-only source as a child of query_sources that returns the same status fields, publish that result the same way as URLhaus and OTX, then include it in the correlation rules.
  • Point kestra_ui_base_url at the URL analysts already use so optional Slack links open the right execution.
  • Keep new sources read-only. Do not add a task that requests, downloads, or detonates the submitted URL.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.