id: http-security-headers-audit
namespace: company.team
description: |
Probe endpoints for missing security headers (HSTS, CSP, X-Frame-Options,
X-Content-Type-Options, Referrer-Policy) and alert Slack on gaps.
triggers:
- id: weekly_header_audit
type: io.kestra.plugin.core.trigger.Schedule
description: Weekly sweep catches header regressions shipped during the week.
cron: "0 5 * * 2"
disabled: true
inputs:
- id: urls
type: STRING
defaults: "https://example.com,https://api.example.com"
description: Comma-separated URLs to probe.
tasks:
- id: probe_headers
type: io.kestra.plugin.scripts.shell.Commands
description: curl -sI each URL, check the required headers, and emit per-URL
missing lists via the stdout outputs protocol. Network failure reports
all-missing so a broken probe cannot read as a pass.
containerImage: alpine:3.20
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
commands:
- |
apk add --no-cache curl python3 >/dev/null 2>&1
cat > audit.py <<'PYEOF'
import os, json, subprocess
required = ["strict-transport-security", "content-security-policy", "x-frame-options", "x-content-type-options", "referrer-policy"]
rows = []
bad = 0
for url in [u.strip() for u in os.environ.get("URLS", "").split(",") if u.strip()]:
try:
out = subprocess.run(["curl", "-sI", "--max-time", "20", url], capture_output=True, text=True, timeout=30).stdout.lower()
missing = [h for h in required if h not in out]
except Exception:
missing = required[:]
if missing:
bad += 1
rows.append({"url": url, "missing": missing})
print(f"probed {len(rows)} url(s), {bad} with gaps")
print("::" + json.dumps({"outputs": {"probed": len(rows), "with_gaps": bad, "details": json.dumps(rows)}}) + "::")
PYEOF
python3 audit.py
env:
URLS: "{{ inputs.urls }}"
- id: gaps_found
type: io.kestra.plugin.core.flow.If
description: Any missing header means an alert; otherwise log the clean pass.
condition: "{{ outputs.probe_headers.vars.with_gaps > 0 }}"
then:
- id: alert_gaps
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Say which URLs lack which headers so the fix is obvious.
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": ":shield: Header audit: {{ outputs.probe_headers.vars.with_gaps }} of {{ outputs.probe_headers.vars.probed }} URL(s) miss security headers. {{ outputs.probe_headers.vars.details }}"
}
else:
- id: log_clean
type: io.kestra.plugin.core.log.Log
description: Record the pass for the trend.
message: "All probed URLs send the full security header set."
errors:
- id: alert_probe_failure
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Alert when the probe itself fails - no result must ever look like
"all headers present".
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": "Header audit FAILED in flow {{ flow.id }} (execution {{ execution.id }}). Check the task logs."
}
outputs:
- id: header_gaps
type: JSON
description: 'Per-URL missing headers, e.g. [{"url": "https://example.com",
"missing": ["content-security-policy"]}]'
value: "{{ outputs.probe_headers.vars.details }}"