# Security scanning

Security scanning looks for sensitive files and pages your app serves to anyone on the internet: repository metadata, environment and credential files, debug pages, directory listings and forgotten backups. Automated scanners request exactly these paths within minutes of a new site appearing. Security scanning requests them first and tells you if one is readable.

It is off by default for every app, including static sites. A clean result does not mean your app is secure: it only means none of the paths below was exposed when the scan ran.

## Turning it on

Open the app's **Monitoring** tab and switch on **Scan for exposed files** in the **Security scan** section. You need permission to manage apps (owners and admins have it). Turning scanning on or off is recorded in your organization's audit log.

The first scan starts within a few minutes. After that, the app is scanned:

- after each new production release goes live,
- at least once every 24 hours, and
- when you choose **Scan now**. After a scan starts, **Scan now** is available again five minutes later.

Turning scanning off stops further scans and closes any open findings without notifying anyone.

## What is checked

On each public production address of the app (its `nimbusapps.ca` address and, once verified, its [custom domain](custom-domains.md)), a scan requests these paths:

| Path | Kind | Reported when the response |
|---|---|---|
| `/.git/HEAD` | Repository metadata | starts with `ref: refs/` or is a commit hash |
| `/.git/config` | Repository metadata | contains a `[core]` section and is not HTML |
| `/.env` | Credentials | has at least two `NAME=value` lines and is not HTML |
| `/.env.production` | Credentials | has at least two `NAME=value` lines and is not HTML |
| `/.npmrc` | Credentials | contains `_authToken=` or `registry=` and is not HTML |
| `/.aws/credentials` | Credentials | has a profile section with `aws_access_key_id` |
| `/server-status` | Diagnostic page | is Apache's server status page |
| `/phpinfo.php` | Diagnostic page | is a `phpinfo()` page |
| `/actuator/env` | Diagnostic page | is Spring Boot's environment endpoint |
| `/` | Directory listing | is an `Index of /` listing |
| `/.DS_Store` | Backup or data file | is a macOS folder metadata file |
| `/backup.sql` | Backup or data file | contains `CREATE TABLE` or `INSERT INTO` and is not HTML |
| `/dump.sql` | Backup or data file | contains `CREATE TABLE` or `INSERT INTO` and is not HTML |
| `/db.sqlite3` | Backup or data file | is an SQLite database file |
| `/wp-config.php.bak` | Backup or data file | is a WordPress configuration containing `DB_PASSWORD` |

A path is reported only when the response has a 2xx status and its **content** looks like the real file or page. Redirects, error pages, and apps that answer every path with their home page are never reported. Before checking, the scan requests one random path that cannot exist, and discards any match identical to that response.

Apps that require sign-in are not scanned, and are shown as protected by sign-in rather than as clean. Apps that are stopped or have no live production release are not scanned either.

## How the requests look to your app

Scan requests reach your app the same way a visitor's do, one at a time and a quarter of a second apart: about 16 requests per address per scan. They carry this user agent:

```
NimbusLand-SecurityScan/1 (+https://nimbusland.ca/docs/security-scanning)
```

Scan requests are left out of your app's Traffic and Analytics, and never count toward the **Visitors seeing errors** or **Site slow** alerts, so an app that answers unknown paths with server errors cannot raise an alert because of a scan.

Responses are compared in memory, at most 64 KiB of each, and discarded. Nothing from a response is stored or logged. A finding records only which check matched, on which URL, and when.

## Findings

Each open finding names the URL, what was exposed, why it matters and how to fix it.

- **Owners and admins get an email** when a scan opens findings: one email per scan, listing all of them.
- **Notification destinations** in your organization settings can subscribe to the `security.finding` event. A webhook receives one signed delivery per finding, with the app, the URL, the check, when it opened and a link to the console. Response content is never included.
- **The app's Overview** shows a **Needs attention** item while any finding is open.
- **A finding resolves on its own** when a later scan that reached every path no longer matches it. If a scan could not reach the app, findings stay open and the section says so. Resolved findings stay listed for 30 days.

Each finding notifies once. Resolving a finding notifies nobody.

### Fixing what is found

- **Repository metadata** (`.git`): serve a build or public directory rather than the repository root, or stop your web server from serving `.git`. Rotate anything that was ever committed.
- **Environment and credential files**: remove the file from the served directory and set the values as [environment variables or secrets](secret-env-vars.md) instead. Rotate every credential the file held, and assume it has been read.
- **Diagnostic pages**: delete `phpinfo.php`, turn off Apache's `mod_status` or restrict it to local requests, and stop exposing Spring Boot's `env` endpoint.
- **Directory listing**: turn it off (`autoindex off` in nginx, `Options -Indexes` in Apache) or add an index page.
- **Backups and data files**: move them out of the web root, and treat the data in them as exposed.

## Limits

- Only the paths above are requested. A scan does not crawl, fuzz, sign in, test TLS or security headers, or look at non-production environments.
- Only the custom domain itself is scanned, not its `www` variant. The two normally serve the same app, so an exposure on one is usually on both.
- A scan of one app takes at most about two minutes. An address still being scanned after that is reported as "scan took too long", and its open findings stay open until a scan completes.
- A clean scan reads "no exposed files found among N common checks". It is not a statement that the app is secure.

The always-on [page check](page-check.md) is a separate check that runs for every app.
