Scheduled Scans
The Scheduled Scans page is your per-tenant cron table. One row → one celery-beat task → fresh findings on the cadence you pick. Works for every scan type Cybrium supports — Email Security, M365, SSL/TLS, DAST, VAPT, Secret Scan, NHI, VM, SBOM, VDI, AI Red-Team, and anything we add later.
Where to find it
Settings → Scheduled Scans (or https://app.cybrium.ai/settings?tab=schedules)
What a schedule does
When a schedule fires, the platform:
- Creates a fresh
ScanJobwith the schedule'sscan_type,target, andscope - Dispatches it through the standard scan engine — exactly the same code path as a manual scan
- Persists findings to your tenant's Findings table
- Updates the schedule's
last_run_at,last_run_status, andlast_scan_idso the dashboard reflects reality
There is no hidden side-loop — a scheduled scan is identical to a manual scan, just dispatched by cron.
Creating a schedule
Click New schedule. The drawer asks for:
| Field | What it does |
|---|---|
| Name | Operator-visible label, e.g. Daily Email Security for example.com |
| Description (optional) | Why this schedule exists; who owns it |
| Scan type | Any of the 12+ supported ScanTypes — Email Security, M365, SSL, DAST, VAPT, Secret Scan, NHI, VM, SBOM, VDI, Medical, AI Red-Team |
| Target | Domain, IP, repo URL, credential alias — interpretation depends on scan type. The target hint below the input matches what the picked scan type expects. |
| Cadence | Daily (02:00 UTC) · Weekly (Mon 03:00 UTC) · Monthly (1st @ 04:00 UTC) · Custom cron |
| Cron expression (custom only) | Standard 5-field crontab — 0 */6 * * * = every 6 hours, */30 * * * 1-5 = every 30 min on weekdays |
| Enabled | Uncheck to create disabled; toggle later from the row's pause/play button |
Save. The platform provisions a django-celery-beat.PeriodicTask immediately, and the next firing happens on the next matching tick of the cron.
Row actions
Each schedule row offers four icons:
| Icon | Action |
|---|---|
| Pause / Play | Toggle enabled. Disabling tears down the celery-beat row immediately; re-enabling provisions a fresh one. |
| Run now | Fires the schedule synchronously. Same effect as the cron firing — fresh ScanJob, dispatched through the standard engine. |
| Edit | Re-opens the drawer for editing. Save re-syncs the celery-beat row to match. |
| Delete | Removes the schedule + celery-beat row. Already-running scans complete; historical findings stay in your Findings page. |
Status semantics
The Last run column tells you whether the schedule is producing what you expect:
- Never run — schedule is new and hasn't fired yet
- Running — a scan dispatched by this schedule is currently in flight
- Success — the last run completed cleanly
- Partial — some tools in the scan returned findings but at least one runner errored
- Failed — dispatch itself failed; check the celery-scan logs
Click the row to open the schedule and inspect the linked last scan_id.
API
Every screen action has an API equivalent — the page is a thin wrapper:
GET /api/scans/schedules/ # list (filters: ?scan_type=, ?enabled=)
POST /api/scans/schedules/ # create
GET /api/scans/schedules/<id>/ # retrieve
PATCH /api/scans/schedules/<id>/ # partial update
DELETE /api/scans/schedules/<id>/ # delete + tear down
POST /api/scans/schedules/<id>/run/ # fire now
POST /api/scans/schedules/<id>/toggle/ # flip enabled flag
Sample create:
curl -X POST https://app.cybrium.ai/api/scans/schedules/ \
-H "Content-Type: application/json" \
-b "$AUTH_COOKIE" \
-d '{
"name": "Daily Email Security for example.com",
"scan_type": "email_security",
"target": "example.com",
"cadence": "daily",
"enabled": true,
"scope": { "reputation": true }
}'
Operator playbook
Common setups you can build in 2 minutes each:
- Daily Email Security per tenant domain — Daily, scan_type
email_security, target your apex domain - Weekly SSL/TLS audit of public-facing hosts — Weekly, scan_type
ssl, target the hostname - Daily DAST on a staging URL after deploy — Daily 03:00, scan_type
dast, target the staging URL - Monthly NHI reconciliation across cloud credentials — Monthly, scan_type
nhi, target the credential alias - Hourly secret scan on a hot repo (custom cron) — Custom
0 * * * *, scan_typesecret_scan, target the repo URL - Daily M365 Benchmark + Email Posture — Two schedules sharing the same target, scan types
m365andemail_security
Migrating legacy schedules
Cybrium previously had three per-type schedulers — Cloud Monitoring, Secret Scan, and Continuous Network Scan. Sprint 101 ships a management command that backfills the Secret Scan and Continuous Network rows into the new universal scheduler:
# Dry run first — reports what would change
python manage.py backfill_scan_schedules --dry-run
# All tenants
python manage.py backfill_scan_schedules
# One tenant
python manage.py backfill_scan_schedules --tenant <schema_name>
The backfill is idempotent — re-running it is safe; each migrated row is tagged with scope.source_legacy_model + scope.source_legacy_id so it's never duplicated. After creating the new row, the legacy celery-beat task is disabled (not deleted) so scans never double-fire. The original legacy DB rows stay intact for audit and rollback.
Cloud Monitoring schedules are intentionally not backfilled yet — their dispatch is keyed on ScanJob.source_type='cloud' rather than scan_type, and the universal scheduler needs an additional field before it can carry them. That follow-up is tracked.
Limits
- Default cadences fire at fixed UTC times (
02:00/Mon 03:00/1st 04:00); use Custom cron if you need a specific local-time window - One ScanJob per fire — schedules don't chain (the Adversary engine handles multi-phase orchestration separately)
- Custom cron is parsed as a standard 5-field crontab; 6-field with seconds is not supported
- The schedule's
targetis a string today — multi-target scheduling uses thetargetsJSON list field via the API (the UI exposes a single input for now)