bfsg-scanner 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/run.d.ts +29 -0
- package/dist/cli/run.js +202 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +3 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/load.d.ts +12 -0
- package/dist/config/load.js +47 -0
- package/dist/config/load.js.map +1 -0
- package/dist/config/schema.d.ts +51 -0
- package/dist/config/schema.js +16 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/discovery/crawl.d.ts +21 -0
- package/dist/discovery/crawl.js +79 -0
- package/dist/discovery/crawl.js.map +1 -0
- package/dist/discovery/discover-site.d.ts +26 -0
- package/dist/discovery/discover-site.js +50 -0
- package/dist/discovery/discover-site.js.map +1 -0
- package/dist/discovery/discover.d.ts +25 -0
- package/dist/discovery/discover.js +81 -0
- package/dist/discovery/discover.js.map +1 -0
- package/dist/discovery/index.d.ts +11 -0
- package/dist/discovery/index.js +7 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/discovery/links.d.ts +11 -0
- package/dist/discovery/links.js +69 -0
- package/dist/discovery/links.js.map +1 -0
- package/dist/discovery/robots.d.ts +18 -0
- package/dist/discovery/robots.js +91 -0
- package/dist/discovery/robots.js.map +1 -0
- package/dist/discovery/sitemap.d.ts +18 -0
- package/dist/discovery/sitemap.js +36 -0
- package/dist/discovery/sitemap.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/report/build.d.ts +14 -0
- package/dist/report/build.js +100 -0
- package/dist/report/build.js.map +1 -0
- package/dist/report/clauses.d.ts +32 -0
- package/dist/report/clauses.js +57 -0
- package/dist/report/clauses.js.map +1 -0
- package/dist/report/i18n.d.ts +54 -0
- package/dist/report/i18n.js +95 -0
- package/dist/report/i18n.js.map +1 -0
- package/dist/report/index.d.ts +12 -0
- package/dist/report/index.js +8 -0
- package/dist/report/index.js.map +1 -0
- package/dist/report/pdf.d.ts +12 -0
- package/dist/report/pdf.js +25 -0
- package/dist/report/pdf.js.map +1 -0
- package/dist/report/render.d.ts +8 -0
- package/dist/report/render.js +204 -0
- package/dist/report/render.js.map +1 -0
- package/dist/report/types.d.ts +78 -0
- package/dist/report/types.js +2 -0
- package/dist/report/types.js.map +1 -0
- package/dist/report/verdict.d.ts +20 -0
- package/dist/report/verdict.js +28 -0
- package/dist/report/verdict.js.map +1 -0
- package/dist/report/write.d.ts +21 -0
- package/dist/report/write.js +35 -0
- package/dist/report/write.js.map +1 -0
- package/dist/scan/index.d.ts +2 -0
- package/dist/scan/index.js +2 -0
- package/dist/scan/index.js.map +1 -0
- package/dist/scan/scan.d.ts +2 -0
- package/dist/scan/scan.js +82 -0
- package/dist/scan/scan.js.map +1 -0
- package/dist/scan/types.d.ts +56 -0
- package/dist/scan/types.js +2 -0
- package/dist/scan/types.js.map +1 -0
- package/dist/shared/index.d.ts +4 -0
- package/dist/shared/index.js +5 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/package-info.d.ts +10 -0
- package/dist/shared/package-info.js +11 -0
- package/dist/shared/package-info.js.map +1 -0
- package/dist/shared/pool.d.ts +7 -0
- package/dist/shared/pool.js +25 -0
- package/dist/shared/pool.js.map +1 -0
- package/dist/shared/rate-limiter.d.ts +11 -0
- package/dist/shared/rate-limiter.js +26 -0
- package/dist/shared/rate-limiter.js.map +1 -0
- package/dist/shared/user-agent.d.ts +11 -0
- package/dist/shared/user-agent.js +12 -0
- package/dist/shared/user-agent.js.map +1 -0
- package/docs/SCANNING-ETHICS.md +52 -0
- package/docs/adr/0001-page-discovery-strategy.md +74 -0
- package/docs/adr/0002-json-report-output.md +54 -0
- package/docs/adr/0003-failon-threshold-and-exit-codes.md +72 -0
- package/docs/adr/0004-clause-mapping-from-axe-tags.md +81 -0
- package/docs/adr/0005-html-report-rendering.md +71 -0
- package/docs/adr/0006-pdf-report-rendering.md +58 -0
- package/docs/adr/0007-cli-argument-surface.md +73 -0
- package/docs/adr/0008-report-json-schema.md +59 -0
- package/package.json +73 -0
- package/schema/report.v1.json +156 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
|
|
5
|
+
follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
Nothing yet.
|
|
10
|
+
|
|
11
|
+
## [0.1.0] - 2026-09-01
|
|
12
|
+
|
|
13
|
+
First release.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Site discovery: `sitemap.xml` first, then a `robots.txt`-respecting
|
|
18
|
+
breadth-first crawl, bounded by `maxPages` and `excludePaths`.
|
|
19
|
+
- Scan engine: axe-core in Chromium via Playwright, with a concurrency pool,
|
|
20
|
+
a per-host rate limit, retries, and an identifiable User-Agent.
|
|
21
|
+
- `report.json` — versioned, self-describing (`schemaVersion: 1`), with a
|
|
22
|
+
shipped JSON Schema at `schema/report.v1.json`.
|
|
23
|
+
- Clause mapping: every finding is tagged with its WCAG success criteria and
|
|
24
|
+
EN 301 549 clauses, read from axe-core rule metadata; the report rolls up
|
|
25
|
+
the distinct breached clauses.
|
|
26
|
+
- `failOn` severity threshold driving the process exit code (`0`/`1`), plus
|
|
27
|
+
`2` for bad configuration and `3` for an incomplete scan.
|
|
28
|
+
- Localised `report.html` (German or English) — a self-contained page — and
|
|
29
|
+
`report.pdf` (the HTML printed to A4).
|
|
30
|
+
- CLI: a positional `url` argument for a one-page scan without a config
|
|
31
|
+
file, `--config`, `--fail-on`, `--report-language`, `--output-dir`,
|
|
32
|
+
`--format`, `--help`, `--version`.
|
|
33
|
+
- `reportFormats` config key / `--format` flag to choose which report files
|
|
34
|
+
are written.
|
|
35
|
+
- Distributable package: a `bfsg-scanner` bin and a `dist` build that
|
|
36
|
+
excludes tests.
|
|
37
|
+
|
|
38
|
+
[Unreleased]: https://github.com/MihaelaAghirculesei/bfsg-scanner/compare/v0.1.0...HEAD
|
|
39
|
+
[0.1.0]: https://github.com/MihaelaAghirculesei/bfsg-scanner/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mihaela Aghirculesei
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# bfsg-scanner
|
|
2
|
+
|
|
3
|
+
Automated **WCAG 2.1 AA** accessibility scanning for a whole site, built on
|
|
4
|
+
[Playwright](https://playwright.dev) and [axe-core](https://github.com/dequelabs/axe-core).
|
|
5
|
+
Every finding is mapped to the clause it breaches in
|
|
6
|
+
[EN 301 549](https://www.etsi.org/deliver/etsi_en/301500_301599/301549/) and,
|
|
7
|
+
through it, Germany's [BFSG](https://www.gesetze-im-internet.de/bfsg/) — so a
|
|
8
|
+
run produces a citeable compliance report, not just a list of rule IDs.
|
|
9
|
+
|
|
10
|
+
Requires **Node.js ≥ 24**.
|
|
11
|
+
|
|
12
|
+
## What it does
|
|
13
|
+
|
|
14
|
+
1. **Discovers** the pages of a site — `sitemap.xml` first, then a
|
|
15
|
+
`robots.txt`-respecting breadth-first crawl, bounded by `maxPages`.
|
|
16
|
+
2. **Scans** each page in a real Chromium via axe-core, with a concurrency
|
|
17
|
+
pool and a per-host rate limit, sending an identifiable User-Agent.
|
|
18
|
+
3. **Reports** the result as `report.json`, `report.html` (German or English),
|
|
19
|
+
and `report.pdf`, with each violation tagged with its WCAG success
|
|
20
|
+
criteria and EN 301 549 clauses.
|
|
21
|
+
4. **Exits non-zero** when violations reach a configurable severity
|
|
22
|
+
threshold, so a CI job can gate a merge on accessibility.
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npx bfsg-scanner https://example.de
|
|
28
|
+
|
|
29
|
+
# scan one page, JSON only, fail the process on any "critical" finding:
|
|
30
|
+
npx bfsg-scanner https://example.de --format json --fail-on critical
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The first run needs a Chromium build for Playwright:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npx playwright install chromium
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Install it for repeated use with `npm i -g bfsg-scanner`.
|
|
40
|
+
|
|
41
|
+
## From source
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
git clone https://github.com/MihaelaAghirculesei/bfsg-scanner.git
|
|
45
|
+
cd bfsg-scanner
|
|
46
|
+
npm install
|
|
47
|
+
npx playwright install chromium
|
|
48
|
+
npm run build
|
|
49
|
+
node dist/cli/index.js --help # or `npm link` for the bfsg-scanner command
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
bfsg-scanner [url] [options]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| Argument / option | Meaning |
|
|
59
|
+
|--------------------------|---------|
|
|
60
|
+
| `url` | Base URL to scan. Omit it to load a config file instead. |
|
|
61
|
+
| `-c, --config <path>` | YAML config file (default: `bfsg.config.yaml`). Not allowed together with a `url`. |
|
|
62
|
+
| `--fail-on <level>` | `critical` \| `serious` \| `moderate` \| `minor` (default: `serious`). |
|
|
63
|
+
| `--report-language <l>` | `de` \| `en` (default: `de`). |
|
|
64
|
+
| `--output-dir <dir>` | Directory for the report files (default: `reports`). |
|
|
65
|
+
| `--format <list>` | Comma-separated subset of `json,html,pdf` (default: all three). |
|
|
66
|
+
| `-h, --help` | Show help and exit. |
|
|
67
|
+
| `-V, --version` | Print the version and exit. |
|
|
68
|
+
|
|
69
|
+
A `url` argument scans that one page with defaults; anything more (a page
|
|
70
|
+
budget, excluded paths, a custom rule set) needs a config file.
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
A YAML file — `bfsg.config.yaml` by default, or `--config <path>`. Only
|
|
75
|
+
`baseUrl` is required; every other key falls back to the default shown.
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
baseUrl: "https://example.de" # required
|
|
79
|
+
|
|
80
|
+
maxPages: 50 # sitemap + crawl combined
|
|
81
|
+
wcagTags: # axe rule sets to run
|
|
82
|
+
- wcag2a
|
|
83
|
+
- wcag2aa
|
|
84
|
+
- wcag21aa
|
|
85
|
+
excludePaths: [] # URL-path globs to skip while crawling
|
|
86
|
+
outputDir: reports # where the report files are written
|
|
87
|
+
failOn: serious # critical | serious | moderate | minor
|
|
88
|
+
reportLanguage: de # de | en — language of the HTML/PDF report
|
|
89
|
+
reportFormats: # any non-empty subset of json | html | pdf
|
|
90
|
+
- json
|
|
91
|
+
- html
|
|
92
|
+
- pdf
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
CLI flags override the matching config key. See the annotated
|
|
96
|
+
[`bfsg.config.yaml`](./bfsg.config.yaml) in this repo for a working example.
|
|
97
|
+
|
|
98
|
+
## Output
|
|
99
|
+
|
|
100
|
+
Written to `outputDir`, overwritten each run:
|
|
101
|
+
|
|
102
|
+
- **`report.json`** — the machine-readable result. Its shape is described by
|
|
103
|
+
[`schema/report.v1.json`](./schema/report.v1.json) (JSON Schema, shipped
|
|
104
|
+
with the package); `schemaVersion` is `1`.
|
|
105
|
+
- **`report.html`** — a self-contained page (no external assets) leading with
|
|
106
|
+
the pass/fail verdict, the breached WCAG / EN 301 549 clauses, and the
|
|
107
|
+
findings per page.
|
|
108
|
+
- **`report.pdf`** — the HTML report printed to A4, for filing as the
|
|
109
|
+
compliance record.
|
|
110
|
+
|
|
111
|
+
The terminal prints a summary and the distinct breached clauses.
|
|
112
|
+
|
|
113
|
+
## Exit codes
|
|
114
|
+
|
|
115
|
+
| Code | Meaning |
|
|
116
|
+
|------|---------|
|
|
117
|
+
| `0` | Scan completed; nothing at or above `--fail-on`. |
|
|
118
|
+
| `1` | Scan completed; violations at or above `--fail-on`. |
|
|
119
|
+
| `2` | Invalid arguments or configuration. |
|
|
120
|
+
| `3` | No pages discovered, or a page could not be scanned. |
|
|
121
|
+
|
|
122
|
+
`3` outranks `1`: a run with unreachable pages scanned an incomplete site, so
|
|
123
|
+
"no violations found" would be a claim the data cannot support.
|
|
124
|
+
|
|
125
|
+
## How the clause mapping works
|
|
126
|
+
|
|
127
|
+
axe-core's rule metadata already carries the mapping: a rule tagged
|
|
128
|
+
`wcag143` checks WCAG SC 1.4.3, `EN-9.1.4.3` maps it to that EN 301 549
|
|
129
|
+
clause. The scanner reads those tags rather than maintaining a hand-authored
|
|
130
|
+
table. § 4 BFSG presumes conformity for content that meets the relevant
|
|
131
|
+
harmonised standard — for the web, EN 301 549, whose Chapter 9 references
|
|
132
|
+
WCAG 2.1 level A/AA — so the EN 301 549 clauses in the report are the
|
|
133
|
+
corresponding citations. This is not legal advice.
|
|
134
|
+
|
|
135
|
+
## Responsible use
|
|
136
|
+
|
|
137
|
+
This tool sends real traffic to whatever site you point it at. Scan only
|
|
138
|
+
sites you own or are authorised to test. See
|
|
139
|
+
[`docs/SCANNING-ETHICS.md`](./docs/SCANNING-ETHICS.md).
|
|
140
|
+
|
|
141
|
+
## Design decisions
|
|
142
|
+
|
|
143
|
+
Each significant choice is recorded as an ADR in [`docs/adr/`](./docs/adr).
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,OAAO,CAAC,QAAQ,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ScanDeps } from '../scan/index.js';
|
|
2
|
+
interface CliArgs {
|
|
3
|
+
readonly help: boolean;
|
|
4
|
+
readonly version: boolean;
|
|
5
|
+
/** Explicit --config path, if given. */
|
|
6
|
+
readonly configPath?: string;
|
|
7
|
+
/** Positional base URL, if given. */
|
|
8
|
+
readonly url?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Config fields overridden on the command line, still as raw values —
|
|
11
|
+
* `configSchema` is the single place they get validated.
|
|
12
|
+
*/
|
|
13
|
+
readonly overrides: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Exit code contract:
|
|
17
|
+
* 0 - scan completed, nothing at or above the `failOn` threshold
|
|
18
|
+
* 1 - scan completed, violations at or above `failOn` were found
|
|
19
|
+
* 2 - invalid arguments or configuration
|
|
20
|
+
* 3 - no pages were discovered, or one or more pages could not be scanned
|
|
21
|
+
*
|
|
22
|
+
* 3 outranks 1: a run with unreachable pages scanned an incomplete site, so
|
|
23
|
+
* "no violations found" would be a claim the data cannot support. The
|
|
24
|
+
* report is still written first, so partial results and failure entries
|
|
25
|
+
* survive either way.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseCliArgs(argv: readonly string[]): CliArgs;
|
|
28
|
+
export declare function run(argv: readonly string[], deps?: ScanDeps): Promise<number>;
|
|
29
|
+
export {};
|
package/dist/cli/run.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { chromium } from 'playwright';
|
|
3
|
+
import { ConfigError, loadConfig, parseConfig } from '../config/index.js';
|
|
4
|
+
import { discoverSite } from '../discovery/index.js';
|
|
5
|
+
import { buildReport, renderHtmlReport, renderPdfReport, writeHtmlReport, writePdfReport, writeReport, } from '../report/index.js';
|
|
6
|
+
import { scan } from '../scan/index.js';
|
|
7
|
+
import { toolInfo } from '../shared/index.js';
|
|
8
|
+
const DEFAULT_CONFIG_PATH = 'bfsg.config.yaml';
|
|
9
|
+
const HELP_TEXT = `bfsg-scanner — WCAG 2.1 AA / EN 301 549 / BFSG accessibility scanner
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
bfsg-scanner [url] [options]
|
|
13
|
+
|
|
14
|
+
Arguments:
|
|
15
|
+
url Base URL to scan. Omit to load a config file instead.
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
-c, --config <path> YAML config file (default: ${DEFAULT_CONFIG_PATH}).
|
|
19
|
+
Not allowed together with a url argument.
|
|
20
|
+
--fail-on <level> critical | serious | moderate | minor (default: serious).
|
|
21
|
+
--report-language <l> de | en (default: de).
|
|
22
|
+
--output-dir <dir> Directory for the report files (default: reports).
|
|
23
|
+
--format <list> Comma-separated subset of json,html,pdf (default: all three).
|
|
24
|
+
-h, --help Show this help and exit.
|
|
25
|
+
-V, --version Print the version and exit.
|
|
26
|
+
|
|
27
|
+
Exit codes:
|
|
28
|
+
0 scan completed, nothing at or above the fail-on threshold
|
|
29
|
+
1 scan completed, violations at or above fail-on
|
|
30
|
+
2 invalid arguments or configuration
|
|
31
|
+
3 no pages discovered, or a page could not be scanned
|
|
32
|
+
`;
|
|
33
|
+
const OPTIONS = {
|
|
34
|
+
help: { type: 'boolean', short: 'h' },
|
|
35
|
+
version: { type: 'boolean', short: 'V' },
|
|
36
|
+
config: { type: 'string', short: 'c' },
|
|
37
|
+
'fail-on': { type: 'string' },
|
|
38
|
+
'report-language': { type: 'string' },
|
|
39
|
+
'output-dir': { type: 'string' },
|
|
40
|
+
format: { type: 'string' },
|
|
41
|
+
};
|
|
42
|
+
function safeParseArgs(argv) {
|
|
43
|
+
try {
|
|
44
|
+
return parseArgs({ args: [...argv], options: OPTIONS, allowPositionals: true });
|
|
45
|
+
}
|
|
46
|
+
catch (cause) {
|
|
47
|
+
throw new ConfigError(cause.message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Exit code contract:
|
|
52
|
+
* 0 - scan completed, nothing at or above the `failOn` threshold
|
|
53
|
+
* 1 - scan completed, violations at or above `failOn` were found
|
|
54
|
+
* 2 - invalid arguments or configuration
|
|
55
|
+
* 3 - no pages were discovered, or one or more pages could not be scanned
|
|
56
|
+
*
|
|
57
|
+
* 3 outranks 1: a run with unreachable pages scanned an incomplete site, so
|
|
58
|
+
* "no violations found" would be a claim the data cannot support. The
|
|
59
|
+
* report is still written first, so partial results and failure entries
|
|
60
|
+
* survive either way.
|
|
61
|
+
*/
|
|
62
|
+
export function parseCliArgs(argv) {
|
|
63
|
+
const { values, positionals } = safeParseArgs(argv);
|
|
64
|
+
if (positionals.length > 1) {
|
|
65
|
+
throw new ConfigError(`Expected at most one URL argument, got ${positionals.length}.`);
|
|
66
|
+
}
|
|
67
|
+
const url = positionals[0];
|
|
68
|
+
if (url !== undefined && values.config !== undefined) {
|
|
69
|
+
throw new ConfigError('Pass a URL argument or --config, not both.');
|
|
70
|
+
}
|
|
71
|
+
const overrides = {};
|
|
72
|
+
if (values['fail-on'] !== undefined) {
|
|
73
|
+
overrides.failOn = values['fail-on'];
|
|
74
|
+
}
|
|
75
|
+
if (values['report-language'] !== undefined) {
|
|
76
|
+
overrides.reportLanguage = values['report-language'];
|
|
77
|
+
}
|
|
78
|
+
if (values['output-dir'] !== undefined) {
|
|
79
|
+
overrides.outputDir = values['output-dir'];
|
|
80
|
+
}
|
|
81
|
+
if (values.format !== undefined) {
|
|
82
|
+
overrides.reportFormats = values.format
|
|
83
|
+
.split(',')
|
|
84
|
+
.map((part) => part.trim())
|
|
85
|
+
.filter((part) => part.length > 0);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
help: values.help ?? false,
|
|
89
|
+
version: values.version ?? false,
|
|
90
|
+
overrides,
|
|
91
|
+
...(values.config !== undefined ? { configPath: values.config } : {}),
|
|
92
|
+
...(url !== undefined ? { url } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function resolveConfig(cli) {
|
|
96
|
+
if (cli.url !== undefined) {
|
|
97
|
+
return parseConfig({ baseUrl: cli.url, ...cli.overrides }, 'command-line arguments');
|
|
98
|
+
}
|
|
99
|
+
const fromFile = loadConfig(cli.configPath ?? DEFAULT_CONFIG_PATH);
|
|
100
|
+
if (Object.keys(cli.overrides).length === 0) {
|
|
101
|
+
return fromFile;
|
|
102
|
+
}
|
|
103
|
+
return parseConfig({ ...fromFile, ...cli.overrides }, 'command-line arguments');
|
|
104
|
+
}
|
|
105
|
+
export async function run(argv, deps = {}) {
|
|
106
|
+
let cli;
|
|
107
|
+
try {
|
|
108
|
+
cli = parseCliArgs(argv);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
112
|
+
return 2;
|
|
113
|
+
}
|
|
114
|
+
if (cli.help) {
|
|
115
|
+
console.log(HELP_TEXT);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
if (cli.version) {
|
|
119
|
+
const { name, version } = toolInfo();
|
|
120
|
+
console.log(`${name} ${version}`);
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
let config;
|
|
124
|
+
try {
|
|
125
|
+
config = resolveConfig(cli);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (error instanceof ConfigError) {
|
|
129
|
+
console.error(error.message);
|
|
130
|
+
return 2;
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
const urls = await discoverSite({
|
|
135
|
+
baseUrl: config.baseUrl,
|
|
136
|
+
maxPages: config.maxPages,
|
|
137
|
+
excludePaths: config.excludePaths,
|
|
138
|
+
});
|
|
139
|
+
if (urls.length === 0) {
|
|
140
|
+
console.error(`No scannable pages discovered for ${config.baseUrl}`);
|
|
141
|
+
return 3;
|
|
142
|
+
}
|
|
143
|
+
console.log(`Discovered ${urls.length} page(s) to scan.`);
|
|
144
|
+
// One Chromium for the whole run: the scan uses it, and the PDF report is
|
|
145
|
+
// printed with it. `run` closes only a browser it launched itself, so an
|
|
146
|
+
// injected one (tests) is left to its owner — the same contract `scan` keeps.
|
|
147
|
+
const browser = deps.browser ?? (await chromium.launch());
|
|
148
|
+
const launchedHere = deps.browser === undefined;
|
|
149
|
+
try {
|
|
150
|
+
return await scanAndReport(config, urls, browser);
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
if (launchedHere) {
|
|
154
|
+
await browser.close();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function scanAndReport(config, urls, browser) {
|
|
159
|
+
const result = await scan(urls, { wcagTags: config.wcagTags }, { browser });
|
|
160
|
+
const report = buildReport(result, {
|
|
161
|
+
baseUrl: config.baseUrl,
|
|
162
|
+
wcagTags: config.wcagTags,
|
|
163
|
+
failOn: config.failOn,
|
|
164
|
+
});
|
|
165
|
+
const formats = new Set(config.reportFormats);
|
|
166
|
+
if (formats.has('json')) {
|
|
167
|
+
console.log(`Report written to ${await writeReport(report, config.outputDir)}`);
|
|
168
|
+
}
|
|
169
|
+
if (formats.has('html') || formats.has('pdf')) {
|
|
170
|
+
const html = renderHtmlReport(report, config.reportLanguage);
|
|
171
|
+
if (formats.has('html')) {
|
|
172
|
+
console.log(`HTML report written to ${await writeHtmlReport(html, config.outputDir)}`);
|
|
173
|
+
}
|
|
174
|
+
if (formats.has('pdf')) {
|
|
175
|
+
const pdf = await renderPdfReport(html, browser);
|
|
176
|
+
console.log(`PDF report written to ${await writePdfReport(pdf, config.outputDir)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const failedPages = result.pages.filter((page) => page.status === 'error');
|
|
180
|
+
if (failedPages.length > 0) {
|
|
181
|
+
for (const page of failedPages) {
|
|
182
|
+
console.error(`Failed to scan ${page.url}: ${page.error}`);
|
|
183
|
+
}
|
|
184
|
+
return 3;
|
|
185
|
+
}
|
|
186
|
+
console.log(`Scanned ${report.summary.pagesScanned} page(s), ${report.summary.totalViolations} rule(s) violated.`);
|
|
187
|
+
const { breachedSuccessCriteria, breachedEn301549Clauses } = report.summary;
|
|
188
|
+
if (breachedSuccessCriteria.length > 0) {
|
|
189
|
+
console.log(`WCAG 2.1 SC breached: ${breachedSuccessCriteria.join(', ')}`);
|
|
190
|
+
console.log(`EN 301 549 clauses breached: ${breachedEn301549Clauses.join(', ')}`);
|
|
191
|
+
}
|
|
192
|
+
const { verdict } = report;
|
|
193
|
+
if (verdict.unrankedViolations > 0) {
|
|
194
|
+
console.warn(`${verdict.unrankedViolations} violation(s) carry no impact rating and were excluded from the ${config.failOn} threshold. Review them in the report.`);
|
|
195
|
+
}
|
|
196
|
+
if (!verdict.passed) {
|
|
197
|
+
console.error(`${verdict.violationsAtOrAboveThreshold} violation(s) at or above "${config.failOn}".`);
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,EAEL,YAAY,EACZ,YAAY,EAEZ,kBAAkB,EAClB,oBAAoB,EACpB,aAAa,GACd,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Config } from './schema.js';
|
|
2
|
+
export declare class ConfigError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Validates an already-parsed config object against the schema, applying
|
|
7
|
+
* defaults. Throws {@link ConfigError} with all issues listed on failure.
|
|
8
|
+
* `source` names where the object came from, for the error message
|
|
9
|
+
* (e.g. a quoted file path, or "command-line arguments").
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseConfig(raw: unknown, source: string): Config;
|
|
12
|
+
export declare function loadConfig(path: string): Config;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { parse as parseYaml } from 'yaml';
|
|
3
|
+
import { configSchema } from './schema.js';
|
|
4
|
+
export class ConfigError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'ConfigError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Validates an already-parsed config object against the schema, applying
|
|
12
|
+
* defaults. Throws {@link ConfigError} with all issues listed on failure.
|
|
13
|
+
* `source` names where the object came from, for the error message
|
|
14
|
+
* (e.g. a quoted file path, or "command-line arguments").
|
|
15
|
+
*/
|
|
16
|
+
export function parseConfig(raw, source) {
|
|
17
|
+
const result = configSchema.safeParse(raw);
|
|
18
|
+
if (!result.success) {
|
|
19
|
+
throw new ConfigError(formatIssues(source, result.error));
|
|
20
|
+
}
|
|
21
|
+
return result.data;
|
|
22
|
+
}
|
|
23
|
+
export function loadConfig(path) {
|
|
24
|
+
let raw;
|
|
25
|
+
try {
|
|
26
|
+
raw = readFileSync(path, 'utf8');
|
|
27
|
+
}
|
|
28
|
+
catch (cause) {
|
|
29
|
+
throw new ConfigError(`Cannot read config file at "${path}": ${cause.message}`);
|
|
30
|
+
}
|
|
31
|
+
let parsed;
|
|
32
|
+
try {
|
|
33
|
+
parsed = parseYaml(raw);
|
|
34
|
+
}
|
|
35
|
+
catch (cause) {
|
|
36
|
+
throw new ConfigError(`Cannot parse "${path}" as YAML: ${cause.message}`);
|
|
37
|
+
}
|
|
38
|
+
return parseConfig(parsed, `"${path}"`);
|
|
39
|
+
}
|
|
40
|
+
function formatIssues(source, error) {
|
|
41
|
+
const lines = error.issues.map((issue) => {
|
|
42
|
+
const field = issue.path.length > 0 ? issue.path.join('.') : '(root)';
|
|
43
|
+
return ` - ${field}: ${issue.message}`;
|
|
44
|
+
});
|
|
45
|
+
return `Invalid config (${source}):\n${lines.join('\n')}`;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=load.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load.js","sourceRoot":"","sources":["../../src/config/load.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAE1C,OAAO,EAAe,YAAY,EAAE,MAAM,aAAa,CAAC;AAExD,MAAM,OAAO,WAAY,SAAQ,KAAK;IACpC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;IAC5B,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,GAAY,EAAE,MAAc;IACtD,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAC3C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,WAAW,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACnC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,WAAW,CAAC,+BAA+B,IAAI,MAAO,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;IAC7F,CAAC;IAED,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,WAAW,CAAC,iBAAiB,IAAI,cAAe,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;IACvF,CAAC;IAED,OAAO,WAAW,CAAC,MAAM,EAAE,IAAI,IAAI,GAAG,CAAC,CAAC;AAC1C,CAAC;AAED,SAAS,YAAY,CAAC,MAAc,EAAE,KAAiB;IACrD,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QACvC,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;QACtE,OAAO,OAAO,KAAK,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;IAC1C,CAAC,CAAC,CAAC;IACH,OAAO,mBAAmB,MAAM,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;AAC5D,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const wcagTagSchema: z.ZodEnum<{
|
|
3
|
+
wcag2a: "wcag2a";
|
|
4
|
+
wcag2aa: "wcag2aa";
|
|
5
|
+
wcag21a: "wcag21a";
|
|
6
|
+
wcag21aa: "wcag21aa";
|
|
7
|
+
}>;
|
|
8
|
+
export declare const failOnSchema: z.ZodEnum<{
|
|
9
|
+
critical: "critical";
|
|
10
|
+
serious: "serious";
|
|
11
|
+
moderate: "moderate";
|
|
12
|
+
minor: "minor";
|
|
13
|
+
}>;
|
|
14
|
+
export declare const reportLanguageSchema: z.ZodEnum<{
|
|
15
|
+
de: "de";
|
|
16
|
+
en: "en";
|
|
17
|
+
}>;
|
|
18
|
+
export declare const reportFormatSchema: z.ZodEnum<{
|
|
19
|
+
json: "json";
|
|
20
|
+
html: "html";
|
|
21
|
+
pdf: "pdf";
|
|
22
|
+
}>;
|
|
23
|
+
export declare const configSchema: z.ZodObject<{
|
|
24
|
+
baseUrl: z.ZodURL;
|
|
25
|
+
maxPages: z.ZodDefault<z.ZodNumber>;
|
|
26
|
+
wcagTags: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
27
|
+
wcag2a: "wcag2a";
|
|
28
|
+
wcag2aa: "wcag2aa";
|
|
29
|
+
wcag21a: "wcag21a";
|
|
30
|
+
wcag21aa: "wcag21aa";
|
|
31
|
+
}>>>;
|
|
32
|
+
excludePaths: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
33
|
+
outputDir: z.ZodDefault<z.ZodString>;
|
|
34
|
+
failOn: z.ZodDefault<z.ZodEnum<{
|
|
35
|
+
critical: "critical";
|
|
36
|
+
serious: "serious";
|
|
37
|
+
moderate: "moderate";
|
|
38
|
+
minor: "minor";
|
|
39
|
+
}>>;
|
|
40
|
+
reportLanguage: z.ZodDefault<z.ZodEnum<{
|
|
41
|
+
de: "de";
|
|
42
|
+
en: "en";
|
|
43
|
+
}>>;
|
|
44
|
+
reportFormats: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
45
|
+
json: "json";
|
|
46
|
+
html: "html";
|
|
47
|
+
pdf: "pdf";
|
|
48
|
+
}>>>;
|
|
49
|
+
}, z.core.$strict>;
|
|
50
|
+
export type Config = z.infer<typeof configSchema>;
|
|
51
|
+
export type ReportFormat = z.infer<typeof reportFormatSchema>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const wcagTagSchema = z.enum(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']);
|
|
3
|
+
export const failOnSchema = z.enum(['critical', 'serious', 'moderate', 'minor']);
|
|
4
|
+
export const reportLanguageSchema = z.enum(['de', 'en']);
|
|
5
|
+
export const reportFormatSchema = z.enum(['json', 'html', 'pdf']);
|
|
6
|
+
export const configSchema = z.strictObject({
|
|
7
|
+
baseUrl: z.url(),
|
|
8
|
+
maxPages: z.number().int().positive().max(1000).default(50),
|
|
9
|
+
wcagTags: z.array(wcagTagSchema).min(1).default(['wcag2a', 'wcag2aa', 'wcag21aa']),
|
|
10
|
+
excludePaths: z.array(z.string()).default([]),
|
|
11
|
+
outputDir: z.string().min(1).default('reports'),
|
|
12
|
+
failOn: failOnSchema.default('serious'),
|
|
13
|
+
reportLanguage: reportLanguageSchema.default('de'),
|
|
14
|
+
reportFormats: z.array(reportFormatSchema).min(1).default(['json', 'html', 'pdf']),
|
|
15
|
+
});
|
|
16
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC;AAElF,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;AAEjF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AAEzD,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;AAElE,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,YAAY,CAAC;IACzC,OAAO,EAAE,CAAC,CAAC,GAAG,EAAE;IAChB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC3D,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;IAClF,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC7C,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC;IAC/C,MAAM,EAAE,YAAY,CAAC,OAAO,CAAC,SAAS,CAAC;IACvC,cAAc,EAAE,oBAAoB,CAAC,OAAO,CAAC,IAAI,CAAC;IAClD,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;CACnF,CAAC,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface CrawlOptions {
|
|
2
|
+
readonly baseUrl: string;
|
|
3
|
+
readonly maxPages: number;
|
|
4
|
+
readonly excludePaths?: readonly string[];
|
|
5
|
+
/** Injectable for tests; defaults to the global fetch. */
|
|
6
|
+
readonly fetchFn?: typeof fetch;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Breadth-first fallback crawl, used when a site has no sitemap. Starts at
|
|
10
|
+
* baseUrl, follows same-origin links in document order, and stops once
|
|
11
|
+
* maxPages pages have been collected. robots.txt, the configured
|
|
12
|
+
* excludePaths, and non-page assets (mailto:, PDFs, images, ...) are all
|
|
13
|
+
* applied identically to the seed URL and to every discovered link, so a
|
|
14
|
+
* disallowed or excluded home page yields zero results rather than being
|
|
15
|
+
* scanned anyway.
|
|
16
|
+
*
|
|
17
|
+
* Deterministic: links are visited in a plain FIFO queue in the order they
|
|
18
|
+
* appear in each page's HTML, so the same input always produces the same
|
|
19
|
+
* page list in the same order.
|
|
20
|
+
*/
|
|
21
|
+
export declare function crawlSite(options: CrawlOptions): Promise<string[]>;
|