# SiteVerdict — Passive Website Security Scanner (`siteverdict/siteverdict-passive-security-scanner`) Actor

Passive, non-intrusive security check for sites you own or manage: TLS and certificate expiry, security headers, CSP, cookie flags, exposed .git/.env/backups, directory listing, mixed content, outdated JS libraries (Retire.js). Grade A+ to F, fixes, HTML report, monitoring, alerts.

- **URL**: https://apify.com/siteverdict/siteverdict-passive-security-scanner.md
- **Developed by:** [Aman Thakran](https://apify.com/siteverdict) (community)
- **Categories:** Developer tools, Business
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## SiteVerdict — Passive Website Security Scanner

**Find the security mistakes that get small websites hacked — before someone else does. One run checks your TLS and certificate expiry, security headers, Content-Security-Policy, cookie flags, exposed `.git` / `.env` / backup files, directory listing, mixed content and outdated JavaScript libraries, then gives each site a grade (A+ to F), plain-English fixes with nginx/Apache snippets, and an HTML report you can send to your developer.**

> **Authorised use only.** Scan websites you own or manage, or have written permission to test. The Actor refuses to run until you tick that box. Checks are **passive and non-intrusive**: ordinary HTTPS requests and TLS handshakes (like a browser), **one GET per well-known path**, no exploitation, no fuzzing, no brute force, no login attempts, robots.txt respected by default.

### Three ways teams use it

1. **Agencies and freelancers:** weekly check of every client site; Slack alert when a certificate is about to expire or a `.env` file suddenly becomes public after a deploy.
2. **Small-business owners:** a one-page report that says exactly what to change, ordered by risk.
3. **IT and compliance:** supporting notes for Cyber Essentials / ISO 27001 / PCI DSS preparation (TLS 1.0/1.1 disabled, HSTS, cookie flags, no directory listing), with a dated HTML record per site. This is not an ASV scan, certification or audit.

### Before / after (sample mode, fictional site)

**You give:** `{ "urls": ["https://sample-shop.example"], "confirmAuthorised": true }`

**You get:**

```json
{ "host": "sample-shop.example", "grade": "F", "score": 0,
  "counts": { "critical": 2, "high": 3, "medium": 6, "low": 6, "info": 3 },
  "findings": [
    { "severity": "critical", "title": "Git repository exposed (/.git/)", "detail": "Git HEAD file is readable; the whole repository ... can usually be reconstructed.", "fix": { "summary": "Block /.git/ ... rotate any secrets ever committed." } },
    { "severity": "critical", "title": "Environment file exposed (/.env)", "detail": "A dotenv file with 3 variables is readable (names like APP_KEY…, DB_PASS…). Values were not read into this report." },
    { "severity": "high", "title": "jquery 1.12.4 has known vulnerabilities", "fix": { "summary": "Upgrade jquery to a version not affected (>= 3.5.0)." } },
    { "severity": "medium", "title": "Certificate expires in 9 day(s)" },
    { "severity": "medium", "title": "Legacy TLS 1.0 and TLS 1.1 still accepted", "fix": { "nginx": "ssl_protocols TLSv1.2 TLSv1.3;" } }
  ],
  "reportUrl": "https://api.apify.com/v2/key-value-stores/…/records/REPORT-sample-shop.example" }
```

(Counts illustrative; run sample mode for exact output.)

### What is checked

| Area | Checks |
|---|---|
| **TLS** | handshake, negotiated protocol and cipher, certificate subject/issuer/SANs, days to expiry, chain trust, hostname match, key size, TLS 1.0/1.1 still accepted (two extra ordinary handshakes) |
| **Transport** | HTTP→HTTPS redirect |
| **Headers** | HSTS (max-age, includeSubDomains), clickjacking (X-Frame-Options / frame-ancestors), X-Content-Type-Options, Referrer-Policy, Permissions-Policy, CORS wildcard+credentials, version disclosure (Server, X-Powered-By, X-AspNet-Version) |
| **CSP** | missing / report-only, `unsafe-inline` without nonces/hashes, `unsafe-eval`, wildcard/https:/data: script sources, http: sources, object-src, base-uri |
| **Cookies** | Secure, HttpOnly (session-like names), SameSite, SameSite=None without Secure — names and flags only, values never stored |
| **Content** | mixed active/passive content, forms posting to http://, third-party scripts without Subresource Integrity |
| **Libraries** | 74 JS libraries (jQuery, jQuery UI, Bootstrap, AngularJS, Lodash, Moment, Handlebars, DOMPurify, …) matched from script URLs and file banners against the Retire.js vulnerability data |
| **Exposed files** | `/.git/HEAD`, `/.git/config`, `/.env`, `/.aws/credentials`, `/.htpasswd`, `/server-status`, `/server-info`, `/phpinfo.php`, `/info.php`, `/.DS_Store`, `/.svn/wc.db`, common backup archives/SQL dumps, `wp-config.php` backups, permissive `crossdomain.xml` — strict content signatures (no "soft 404" false alarms), first 4 KB only, redirects not followed, contents never output |
| **Directory listing** | `/uploads/`, `/backup/`, `/backups/`, `/files/`, `/images/`, `/assets/` |
| **Disclosure policy** | `/.well-known/security.txt` (RFC 9116) |

About 35 requests per site, paced at ≥300 ms per host.

### Grades

Start at 100; each distinct issue type deducts (critical 40, high 20, medium 8, low 3). Any critical finding = **F**; any high finding caps the grade at **C**; any medium caps it at **A**. A+ means no medium-or-worse issue.

### Monitoring, alerts and AI fixes

- `monitorStateName` remembers each site's findings: results show **new** and **resolved** findings, grade and certificate changes; `onlyChanged` skips (and does not charge for) unchanged sites.
- Slack / HTTPS webhook alerts for new findings at or above `alertMinSeverity` and for certificates inside `expiryWarnDays`.
- Optional **AI fix snippets** with YOUR Anthropic key: stack-specific config changes (nginx, Apache, Cloudflare, frameworks) for the findings. The AI step receives the host name, the Server header, your stack hint and the finding titles/details (titles may include cookie names); never page content, cookie values or file contents.

### Pricing (pay per event)

| Event | Price | When |
|---|---|---|
| `site-scanned` | $0.02 | a site was scanned and its result delivered (HTML report included) |
| `ai-fix` | $0.02 | AI fix snippets were produced (your Anthropic usage billed to your key) |

Free: every row whose `status` is not `scanned` (see below), unchanged sites with `onlyChanged`, sample mode. **Pre-filled form / no authorisation:** real sites are never scanned without the authorisation tick. If the tick is off and the only targets are reserved example names such as `example.org` (as in the pre-filled form), the Actor runs the sample instead of failing: simulated fictional sites, no network requests, every row has `isSampleData: true`, the status message starts with **SAMPLE DATA**, nothing is charged. Sample mode always reports on the fictional sample sites only, never on a real domain. **Examples:** 10 sites ≈ $0.20; weekly monitoring of 50 client sites ≈ $1.00/week (less with `onlyChanged`).

**Row status (every target gets exactly one row):** `scanned` = full scan, graded, charged `site-scanned`. Not charged and not graded (`grade: null`): `tls-invalid` = the server answered TLS but its certificate failed validation (expired, self-signed/untrusted, wrong hostname); the certificate findings are listed and `httpChecksRan: false` (headers, cookies, content and exposed files could not be checked, because a browser would refuse the page too) · `unreachable` = DNS failure, connection refused or timeout · `refused` = private, loopback, link-local, cloud-metadata or reserved address (at input, in the DNS answer or after a redirect), never contacted · `skipped_budget` = not scanned because your max cost per run was reached. OUTPUT counts each (`scanned`, `tlsInvalid`, `unreachable`, `refused`, `notScannedBudget`, `errors`) and the status message names every non-zero one, e.g. "Done: 2 scanned (2x C), 3 TLS-invalid (certificate problem, HTTP checks not run), 1 unreachable, 0 errors."

### FAQ

**Is this a penetration test?** No. It is an external, passive hygiene check — what a browser or search engine could see. It does not log in, submit forms, inject payloads or guess paths beyond the fixed list.
**Why was a path "skipped-robots"?** Your robots.txt disallows it for crawlers; owners can turn `respectRobotsTxt` off for their own sites.
**Could it miss things?** Yes: server-side vulnerabilities, logic flaws, anything behind login, and libraries bundled into minified files without banners. A good grade is not a guarantee.

### Limits

Port 443/80 only; one site = one host (add subdomains separately); JavaScript is not executed (no browser); exposed-file list is fixed by design. Certificate revocation (OCSP/CRL) is **not** checked: a revoked but otherwise valid certificate is not reported.

### Sources & compliance

- Vulnerability data: **Retire.js** repository (`RetireJS/retire.js`, `repository/jsrepository-v4.json`, fetched 11 Oct 2026), **Apache License 2.0**; a trimmed copy and the licence ship in `src/data/`.
- Guidance referenced: Mozilla SSL Configuration Generator, MDN header docs, OWASP WSTG, RFC 8996 (TLS 1.0/1.1 deprecation), RFC 9116 (security.txt).
- Network safety: honest User-Agent `SiteVerdict/passive-security-scanner (+hello@webrevenuescan.com; authorised passive check)`; SSRF guard (private, loopback, link-local and cloud-metadata addresses refused at input, on every redirect and at connect time); robots.txt respected by default; no bot-protection bypass.
- Legal: authorised use only (UK Computer Misuse Act 1990 and equivalents). No personal data is collected (cookie values and file contents are never stored). Not legal advice; a clean result is not a guarantee of security.

Built by SiteVerdict (BharatFare Travel and Technologies Ltd, UK). Contact: hello@webrevenuescan.com.

# Changelog

This Actor's version history is a separate document: https://apify.com/siteverdict/siteverdict-passive-security-scanner/changelog.md

# Actor input Schema

## `urls` (type: `array`):

Domains or URLs, one per line. Private, internal and cloud-metadata addresses are refused.

## `confirmAuthorised` (type: `boolean`):

Required. Scanning systems without permission may be illegal (e.g. Computer Misuse Act 1990 in the UK).

## `respectRobotsTxt` (type: `boolean`):

On by default: paths disallowed for our crawler are skipped and reported as skipped. Owners may switch it off for their own sites.

## `probeLegacyTls` (type: `boolean`):

Two extra ordinary handshakes.

## `exposureChecks` (type: `boolean`):

One GET per path from a fixed list of ~25 well-known paths; first 4 KB only; contents never output.

## `directoryListing` (type: `boolean`):

Check common folders for directory listing.

## `scriptFetches` (type: `integer`):

First 8 KB of up to N script files referenced by the page (to read version banners).

## `expiryWarnDays` (type: `integer`):

Warn when the certificate expires within (days).

## `generateHtmlReport` (type: `boolean`):

Stored in the run's key-value store; link in each result.

## `monitorStateName` (type: `string`):

Remembers findings per site and reports new/resolved findings and grade changes next run.

## `onlyChanged` (type: `boolean`):

Only output sites that changed.

## `alertMinSeverity` (type: `string`):

Alert on new findings of at least.

## `slackWebhookUrl` (type: `string`):

HTTPS only. Stored encrypted, never logged.

## `webhookUrl` (type: `string`):

HTTPS only; private addresses refused. Stored encrypted, never logged.

## `enableAiFix` (type: `boolean`):

Off by default. Sends the host name, Server header, your stack hint and finding titles/details (titles may include cookie names; never page content or cookie values) to Anthropic with YOUR key and adds stack-specific configuration fixes. Charged only when produced.

## `anthropicApiKey` (type: `string`):

Stored encrypted, never logged or written to output.

## `aiModel` (type: `string`):

Anthropic model.

## `stackHint` (type: `string`):

e.g. nginx + WordPress on Ubuntu, Cloudflare in front

## `concurrency` (type: `integer`):

Sites in parallel.

## `sampleMode` (type: `boolean`):

Sample mode (two simulated fictional sites, no network, no charge). Sample mode only reports on fictional sample sites. If the authorisation tick is off and the only targets are reserved example names (e.g. example.org), the run uses sample mode automatically and every row is marked isSampleData: true. Real sites always need the tick.

## Actor input object example

```json
{
  "urls": [
    "https://example.org"
  ],
  "confirmAuthorised": false,
  "respectRobotsTxt": true,
  "probeLegacyTls": true,
  "exposureChecks": true,
  "directoryListing": true,
  "scriptFetches": 5,
  "expiryWarnDays": 21,
  "generateHtmlReport": true,
  "monitorStateName": "default",
  "onlyChanged": false,
  "alertMinSeverity": "high",
  "enableAiFix": false,
  "aiModel": "claude-sonnet-5-5",
  "concurrency": 2,
  "sampleMode": false
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `output` (type: `string`):

No description

## `files` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        "https://example.org"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("siteverdict/siteverdict-passive-security-scanner").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "urls": ["https://example.org"] }

# Run the Actor and wait for it to finish
run = client.actor("siteverdict/siteverdict-passive-security-scanner").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    "https://example.org"
  ]
}' |
apify call siteverdict/siteverdict-passive-security-scanner --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,siteverdict/siteverdict-passive-security-scanner"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/s0h1WUc0zCTi7kDEj/builds/rnctAcI5wNHGBBdtY/openapi.json
