Feature

Scan authorization guards
enforced before any scan runs

Every scan request passes a compound pre-flight guard: blocklist, monthly quota, verified domain, verification expiry, subdomain plan flag, signed attestation, plan cooldown, and (for authenticated scans) credential binding to a verified domain. Each refusal returns a typed code and a human-readable reason that the API surfaces as a 403, the activity log records, and internal audit can read in the same shape as a successful scan.

No credit card required. Free plan available forever.

Eight pre-flight checks before any scan leaves the platform

Multi-tenant scanning platforms carry a hard requirement that has nothing to do with the quality of the scan itself: every scan that runs has to be authorised, scoped, and audit-evidenced before any traffic leaves the platform. Enterprise security buyers, AppSec leads, GRC owners, and platform security architects ask the same procurement question in different words. How does the platform refuse a scan against a target the customer does not own, has not signed off on, or is not contractually allowed to test. SecPortal answers the question with a compound pre-flight guard that runs before any scanner module fires. The guard is in code, not in policy, so the answer is the same shape on every request.

The guard is shaped as a single function that reads the resolved target, the workspace plan, the verified domains for the workspace, the attestation record for the verified domain, the recent scan history for the cooldown window, and (for authenticated scans) the credential row pinned to the verified domain. Each step has a typed failure code and a human-readable reason, and the scan API surfaces both as a 403 response. Refusal is the same shape as acceptance, which keeps the audit trail uniform and the operator feedback predictable.

The eight-step guard chain

Each step runs in order and short-circuits on the first failure. The order is deliberate: the cheapest checks (blocklist and quota) run first so a clearly refused request does not need a database lookup, and the most context-heavy checks (attestation, cooldown, credential binding) run last so a scan that reaches them has already passed the earlier policy gates.

Step 1

Blocklist check on the resolved target

Before any plan or workspace state is read, the target string is normalised and checked against the platform blocklist. The blocklist refuses SecPortal own domains, critical internet infrastructure (ICANN, IANA, root DNS), financial infrastructure (SWIFT), government and military TLDs (.gov, .mil, .gov.uk, .mod.uk, .police.uk, .nhs.uk), and cloud provider management planes (.amazonaws.com, .azure.com, .azure.net, .googleapis.com, .cloudfront.net). The refusal returns the BLOCKLISTED code so the failure is the same shape as every other guard failure.

Step 2

Monthly scan quota enforced by plan

The guard counts scans already run this month for the workspace and compares against the plan ceiling. Starter sits at 2 scans per month, Pro at 50, Team at 100. A workspace that hits the ceiling receives SCAN_LIMIT with the current usage and the plan name in the message, so the failure surface is the same string the upgrade prompt reads on the dashboard.

Step 3

Verified domain lookup for the root of the target

The guard extracts the root domain from the target, with explicit handling for two-part TLDs like co.uk, com.au, co.jp, com.br, and reads verified_domains for the resolved workspace and root. A missing row returns DOMAIN_NOT_VERIFIED. A row that is not in the verified state returns DOMAIN_NOT_VERIFIED with the current verification_status surfaced in the message, so the operator knows whether to retry verification or fix the DNS record, file upload, or meta tag.

Step 4

Verification expiry on the resolved domain

A verified row with an expires_at in the past returns DOMAIN_EXPIRED. Verification ages out because ownership of a domain can change through an acquisition, a registrar transfer, or a hosting change, so the platform refuses to honour a stale verification rather than accumulate quiet risk in the scan history.

Step 5

Subdomain plan-feature check

If the target is a subdomain of the verified root and the plan does not carry the subdomain scanning feature flag, the guard returns SUBDOMAIN_NOT_ALLOWED. Starter does not permit subdomain scanning; Pro and Team do. The check protects workspaces that intend the root-only behaviour from accidentally fanning out scans across a long subdomain inventory.

Step 6

Per-domain authorisation attestation

A row in scan_attestations scoped to the workspace and the verified domain id is required. A target with no attestation returns NO_ATTESTATION. The attestation is the signed authorisation step that sits next to verification; verification proves administrative control of the domain, the attestation proves authority to authorise scanning. Both artefacts have to be present before the scan is allowed to leave the platform.

Step 7

Plan cooldown window between scans

If the plan carries a non-zero scanCooldownHours value, the guard counts scan_executions for the workspace inside the cooldown window, excluding scans in the blocked or cancelled state. A recent scan inside the window returns COOLDOWN with the cooldown hours and the plan name in the message. Starter carries a 24-hour cooldown; Pro and Team have no cooldown.

Step 8

Authenticated scan credential binding

Authenticated scans run a second guard that requires the plan to allow authenticated scanning, requires the scan_credentials row to belong to the workspace, requires the credential domain_id to point at a verified domain in the same workspace, and requires the credential target_url hostname to match the scan target or its root. A failure on any of these returns AUTH_NOT_ALLOWED, CREDENTIAL_NOT_FOUND, or CREDENTIAL_DOMAIN_MISMATCH, so a credential intended for one application cannot be reused against another.

Ten typed guard codes the scan API returns

The guard returns a typed code with every refusal so the API response, the activity log entry, and the operator dashboard read the same answer. Operators wiring the platform into a runbook can branch on the code; auditors reading the activity log can read the code as a structured fact rather than a free-text message.

BLOCKLISTED

The target is on the platform blocklist. The reason string names the category, so the operator knows whether the block is the cloud control-plane category, the government and military TLD category, the critical infrastructure category, or the platform self-block.

SCAN_LIMIT

The workspace has reached its monthly scan ceiling for the current plan. The failure carries the current usage, the plan ceiling, and the plan name, so the upgrade prompt reads from the same record.

DOMAIN_NOT_VERIFIED

The root of the target is not present in verified_domains for the workspace, or the row exists in a non-verified state. The message names the resolved root domain so the operator knows which verification record to fix.

DOMAIN_EXPIRED

Verification existed for the root domain but the expires_at timestamp has passed. The platform refuses to honour aged verification because ownership of a domain can change after verification was first recorded.

SUBDOMAIN_NOT_ALLOWED

The target is a subdomain of the verified root and the plan does not carry the subdomain scanning feature flag. Starter lands here; Pro and Team carry the flag on.

NO_ATTESTATION

No scan_attestations row exists for the workspace and the verified domain id. The attestation is the signed authority-to-test record; without it, ownership is proven but authorisation is not.

COOLDOWN

A recent scan inside the plan cooldown window is present in scan_executions for the workspace. The message names the cooldown hours and the plan name so the operator knows whether to wait or upgrade.

AUTH_NOT_ALLOWED

Authenticated scanning was requested but the plan does not carry the authenticatedScanning feature flag. Starter lands here; Pro and Team carry the flag on.

CREDENTIAL_NOT_FOUND

The credential id supplied to the scan request does not match a scan_credentials row in the workspace. A credential created in one workspace cannot be used by another, because the lookup is workspace-scoped.

CREDENTIAL_DOMAIN_MISMATCH

The credential references a verified domain that does not exist or is not in the verified state, or the credential target_url hostname does not match the scan target root. A credential intended for app-a.example.com cannot be repointed at app-b.other.com.

Enterprise procurement answers in one place

Enterprise security questionnaires and vendor risk assessments ask a predictable set of pre-scan authorisation questions. The answers below describe the guard the platform actually runs, so the procurement record and the production behaviour are the same record.

How is a scan authorised before it runs?

Every scan request runs through canScanDomain before any module fires. The compound guard refuses the request if the target is on the platform blocklist, if the workspace has hit its monthly scan quota, if the root domain is not verified or has aged out, if the request is for a subdomain on a plan that does not allow subdomain scanning, if no signed attestation exists for the verified domain, or if a recent scan inside the cooldown window is present. The guard returns a single typed code and a human-readable reason; the API surfaces both as a 403.

How is target ownership proved before scanning?

The verified_domains table holds the authoritative record. A domain enters the table through DNS TXT, HTML file upload, or meta tag verification, and the scan guard reads the row by the resolved root domain for the workspace. A target whose root is not present, is in a pending verification state, or has aged past its expires_at timestamp is refused, so a scan request always corresponds to a domain whose administrative control was proven for the workspace.

How does the platform refuse scans of infrastructure the customer does not own?

The blocklist refuses categories the platform will not honour regardless of plan or contract. Government and military TLDs, critical internet infrastructure, financial infrastructure such as SWIFT, cloud provider management planes, and SecPortal own domains are refused before any quota or verification check runs. The refusal returns BLOCKLISTED with a reason that names the category, so the operator knows the refusal is a platform policy and not a configuration mistake.

How is a signed authorisation to test recorded?

A scan_attestations row scoped to the workspace and the verified domain id is required before any scan against the domain is allowed to proceed. The attestation sits next to verification; verification proves administrative control of the asset, the attestation captures the authority-to-test declaration. The pair is the audit answer to an unauthorised-scanning question, because the answer reads as one verified-domain row plus one attestation row plus the activity log entries for the scans that ran under that pair.

How is an authenticated scan credential pinned to a target?

A scan_credentials row carries a domain_id that points at a verified domain in the same workspace and a target_url whose hostname must match the scan target or share its root. A credential created for one application cannot be repointed at another, and a credential whose underlying verified domain has aged out of the verified state cannot be used. The credential gate is a separate guard run on top of the domain guard, so authenticated scans inherit every check the external scan already passes.

How are plan limits enforced as part of the guard chain?

The same compound guard reads the plan and applies the monthly scan ceiling, the subdomain scanning feature flag, and the cooldown window between scans. A workspace cannot exceed the plan it paid for by reaching the scan endpoint directly because the guard runs at the API rather than relying on the dashboard to refuse the action. Downgrades take effect on the next scan because every request reads the current plan, not the plan that was active at signup.

Failure modes the guard refuses to render

The guard refuses before it scans. Each failure below corresponds to a typed code and a human-readable reason; the same shape is returned to the API caller, written to the activity log, and surfaced on the scan dashboard. Refusal is part of the audit trail, not a silent miss.

Unverified root domain

A scan request for a root or subdomain whose root domain is not in verified_domains for the workspace returns DOMAIN_NOT_VERIFIED. The message names the resolved root so the operator can fix the verification record rather than the target string.

Expired verification

A verified row whose expires_at is in the past returns DOMAIN_EXPIRED. The platform refuses to scan against aged verification because ownership of the asset may have changed since the original verification.

Missing attestation

A target whose root domain is verified but has no row in scan_attestations returns NO_ATTESTATION. Verification proves administrative control; the attestation proves authority to test. The platform requires both before allowing a scan to leave.

Subdomain on a plan without subdomain scanning

A target that resolves to a subdomain of the verified root, on a plan whose subdomainScanning flag is off, returns SUBDOMAIN_NOT_ALLOWED. The message names the plan so the upgrade path is obvious.

Inside the plan cooldown window

A scan request inside the cooldown window for the plan returns COOLDOWN with the cooldown hours surfaced. The window is enforced by counting scan_executions in the cooldown range excluding the blocked and cancelled states, so a refused scan does not reset the cooldown.

Credential pointed at a different application

An authenticated scan whose credential domain_id matches a verified domain but whose target_url hostname does not match the scan target or its root returns CREDENTIAL_DOMAIN_MISMATCH. The check is hostname-precise so a credential cannot be repurposed across applications even when both share a parent.

The evidence surfaces the guard chain produces

The pre-flight guard exists to produce a defensible audit answer for every scan that ran and every scan that was refused. The bullets below list the records the platform keeps so internal audit, ISO 27001 surveillance, SOC 2 access review, PCI DSS testing evidence, and any unauthorised-scanning dispute have a single source of truth to read.

  • Every accepted scan request writes a scan_executions row scoped to the workspace, the verified domain id, and the actor on the session
  • Every refused scan request is logged with the guard code and the reason string so the audit trail captures the denial in the same shape as a successful run
  • Every privileged scan action writes to the workspace activity log, which is workspace-scoped and read by the internal audit, ISO 27001 surveillance, and SOC 2 access review questions
  • The attestation that authorised the scan is retrievable from scan_attestations by the workspace and domain id, so the audit answer for "who authorised this scan" is one query
  • The verification record that proved ownership is retrievable from verified_domains for the workspace, so the audit answer for "how was the target proved to belong to the customer" is one query
  • The plan that gated the scan is readable from the workspace row at the time the scan ran, so the audit answer for "which quota was in effect" is deterministic

Tenant contexts the guard chain supports

Internal security teams, AppSec teams handing off domains from engineering, GRC owners producing evidence for a compliance cycle, and security consultancies delivering tests across a client portfolio all read the same guard chain. The state varies (which domains are verified, which attestations are signed, which plan is paid for), but the gate is identical.

Internal security team running a continuous testing programme

The verification records and attestations are managed by the central security team. AppSec and vulnerability management members initiate scans against verified roots and approved subdomains; the guard chain refuses targets that drift outside the approved set, and the activity log captures the refusal so coverage gaps are visible rather than silent.

AppSec team coordinating with engineering and platform

Application teams hand off domains for verification and attestation; AppSec runs the scans. The credential gate keeps authenticated-scan accounts pinned to the application the engineering team intended, so a credential issued for one product cannot be reused against another without a deliberate authorisation step.

GRC owner producing evidence for a compliance cycle

The pre-scan guard chain is the evidence story the GRC owner reads. Verification plus attestation plus the guard codes that refused the out-of-scope targets is the answer to the auditor question about how scanning was authorised, scoped, and policed. The activity log carries the per-scan record on the same workspace.

Security consultancy delivering tests across multiple clients

Each client lands in a workspace with its own verified domains, attestations, and quotas. The guard chain reads workspace-scoped state, so a verified domain in one client workspace does not authorise scans in another. The same pre-flight discipline that holds for an internal team holds across the consulting portfolio.

How the scan guard composes with the rest of the platform

The pre-flight guard is the gate at the API boundary. Each step reads from a primitive the rest of the platform also depends on, and the composition is deliberate so a change in one primitive carries through to the guard without a parallel rewrite.

The domain verification record is the ownership primitive the guard reads. A scan against an unverified root or an expired verification is refused at step three, so the verification record is the first artefact the audit trail points to when an unauthorised-scanning question is asked.

The plan-based limits and quotas layer provides the monthly scan ceiling, the subdomain scanning feature flag, the cooldown window between scans, and the authenticated scanning feature flag. The guard reads the plan for the workspace on every request, so a downgrade or upgrade takes effect on the next scan without a deploy or a cache invalidation.

The encrypted credential storage primitive holds the authenticated-scan credentials the guard pins to a verified domain. The credential gate runs alongside the domain guard for authenticated scans, and the credential cannot be repointed across applications even when both live under the same parent domain.

The activity log records every privileged scan action under the resolved workspace. Accepted scans, refused scans, attestation events, and verification events all land in the same feed, so the audit answer for any pre-scan question is one query, not a join across feature surfaces.

The tenant subdomain isolation model resolves the workspace before the guard chain runs. The verified-domain lookup, attestation row, plan quota, and cooldown count are all scoped to the workspace the middleware resolved, so a request can never accidentally read a verified domain or attestation from a neighbouring tenant.

The API rate limiting primitive sits in front of the scan endpoints with a per-user key, so the guard chain only runs against requests that have already cleared the request-shaping cap. The rate gate stops a runaway script from saturating the scan worker queue inside a single 15-minute window even when the workspace still has plan budget for the month, and the authorisation guard then refuses any target the workspace does not own. The two primitives compose without one replacing the other.

Where to read the discipline behind the gate

The product capability above sits on top of the discipline of authorising a scan before it runs. For the methodology side, see the scan target validation and authorisation article. For the lifecycle of the credential the authenticated guard pins to a verified domain, see the scanner credential rotation and lifecycle article. For the operational workflow that turns scan output into remediation evidence once a scan is authorised and run, see the scanner result triage use case.

The audience pages that map most closely to this gate are the internal security teams page, the AppSec teams page, and the GRC and compliance teams page; each one frames the pre-scan authorisation model in the language the respective buyer uses.

Stop scans against targets the customer does not own

The guard chain runs at the API, not on the dashboard, so a request that bypasses the UI still meets the same eight checks. Refusal is a typed code and an audit log entry, not a silent miss.

No credit card required. Free plan available forever.