diff --git a/README.md b/README.md index 8b5b60e..a9f3145 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,10 @@ red PR job against. - `scripts/check-links.sh [--external-warn]` — resolves every internal link/anchor and probes external URLs (CI runs it with `--external-warn` on PRs and pushes; the weekly - scheduled run and manual dispatch hard-fail on broken external links). + scheduled run and manual dispatch hard-fail on broken external links). Besides every + committed page and the `make-redirects.sh` map, it checks the URI fields of + `.well-known/security.txt` (RFC 9116 `Contact`, `Canonical`, `Policy`, + `Acknowledgments`, `Hiring`), so a rotted security-contact link is reported too. - `scripts/check-links.test.sh` — fixture-based self-test of the link checker; runs offline (fake `curl`). - `node --test scripts/story-dialog.test.mjs` — runs the Share-your-story dialog script diff --git a/scripts/check-links.sh b/scripts/check-links.sh index f4f1fae..fd111cc 100755 --- a/scripts/check-links.sh +++ b/scripts/check-links.sh @@ -75,12 +75,23 @@ extract_redirect_targets() { awk -F'"' '/^[[:space:]]*\[[^]]+\]="/ { split($2, parts, "|"); print "make-redirects.sh\t" parts[1] }' "$redirects" } +# RFC 9116 §2.5: the values of these security.txt fields are URIs. Comment +# lines (#) and non-URI fields (Expires, Preferred-Languages) are left alone. +extract_security_txt_uris() { + local file=".well-known/security.txt" + [[ -f "$ROOT/$file" ]] || return 0 + grep -Ei '^(Contact|Canonical|Policy|Acknowledgments|Hiring):' "$ROOT/$file" | + sed -E -e 's/^[^:]+:[[:space:]]*//' -e 's/[[:space:]]+$//' | + awk -v file="$file" 'NF { print file "\t" $0 }' +} + find "$ROOT" -name '*.html' -not -path "$WORK_DIR/*" -print | sort | while IFS= read -r file; do rel="${file#$ROOT/}" extract_html_links "$rel" done > "$WORK_DIR/links.tsv" extract_redirect_targets >> "$WORK_DIR/links.tsv" +extract_security_txt_uris >> "$WORK_DIR/links.tsv" sort -u "$WORK_DIR/links.tsv" > "$WORK_DIR/links.dedup.tsv" diff --git a/scripts/check-links.test.sh b/scripts/check-links.test.sh index 7d3c030..47b3172 100755 --- a/scripts/check-links.test.sh +++ b/scripts/check-links.test.sh @@ -165,6 +165,47 @@ expect_out 'ERROR external broken: make-redirects\.sh -> https://example\.test/b expect_no_out 'label' "label half is not treated as a URL" expect_out 'External failures: 1\.' "only the broken target counts" +echo "case: .well-known/security.txt URI fields are checked (RFC 9116 Contact/Canonical/Policy/Acknowledgments/Hiring)" +root="$(new_site security-txt)" +echo '

home

' > "$root/index.html" +mkdir -p "$root/.well-known" +cat > "$root/.well-known/security.txt" <<'TXT' +# A comment mentioning https://example.test/broken-comment must be ignored +Contact: mailto:security@example.test +Contact: https://example.test/broken +Expires: 2027-01-01T00:00:00.000Z +Preferred-Languages: en +Canonical: https://hivecommons.dev/.well-known/security.txt +Policy: https://example.test/fine +Acknowledgments: https://example.test/soft404 +Hiring: https://example.test/server-error +TXT +run_checker "$root" +expect_rc 1 "rotten security.txt URIs fail the gate" +expect_out 'ERROR external broken: \.well-known/security\.txt -> https://example\.test/broken \(404' "https Contact is probed and attributed to security.txt" +expect_out 'ERROR external soft-404: \.well-known/security\.txt -> https://example\.test/soft404 \(200' "Acknowledgments is probed" +expect_out 'ERROR external broken: \.well-known/security\.txt -> https://example\.test/server-error \(500' "Hiring is probed" +expect_no_out 'broken-comment' "comment lines are not harvested" +expect_no_out 'mailto' "mailto: Contact is skipped, not flagged" +expect_no_out 'internal target missing' "Canonical resolves to the committed security.txt" +expect_out 'External failures: 3\.' "Policy that resolves is not counted" + +echo "case: security.txt Canonical pointing at a path the tree does not publish is an internal error" +root="$(new_site security-canonical)" +echo '

home

' > "$root/index.html" +mkdir -p "$root/.well-known" +printf 'Contact: mailto:security@example.test\nCanonical: https://hivecommons.dev/.well-known/other.txt\n' > "$root/.well-known/security.txt" +run_checker "$root" --external-warn +expect_rc 1 "unpublished Canonical fails even under --external-warn" +expect_out 'ERROR internal target missing: \.well-known/security\.txt -> https://hivecommons\.dev/\.well-known/other\.txt' "Canonical is resolved against the tree like any site URL" + +echo "case: a site without security.txt is unaffected" +root="$(new_site no-security-txt)" +echo 'home' > "$root/index.html" +run_checker "$root" +expect_rc 0 "no security.txt, no extra links" +expect_out 'Checked 1 unique links\.' "only the page link is counted" + echo "case: external failures — hard 404, 5xx, curl failure, soft-404" root="$(new_site external)" cat > "$root/index.html" <<'HTML'