Repository navigation
140 lines (124 loc) · 6.32 KB
/
Copy pathdeploy.yml
File metadata and controls
140 lines (124 loc) · 6.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
name: Deploy to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
dotnet-quality: 'preview'
- name: Restore
run: dotnet restore src/GitDiary.Client/GitDiary.Client.csproj
# Runs BEFORE publish so a failure blocks the deploy rather than reporting it
# after the fact. SafeMarkdownTests is the load-bearing one: the Markdown
# preview's HTML is injected via innerHTML, and the PAT is in localStorage, so
# an injection there reads the user's credential. A green deploy with those
# tests red would ship exactly the vulnerability they exist to prevent.
- name: Test
run: dotnet test tests/GitDiary.Tests/GitDiary.Tests.csproj -c Release
- name: Publish
run: dotnet publish src/GitDiary.Client/GitDiary.Client.csproj --no-restore -c Release -o publish
# Rewrite <base href="/"> so the app works under the repo subpath on
# <user>.github.io/<repo>/. Historically this used a strict sed pattern
# that silently no-op'd if the tag's spacing or self-closing changed —
# the app would deploy but 404 in the browser. Two hardenings:
# 1. The regex tolerates whitespace variations and optional trailing '/'.
# 2. The Verify step below fails the deploy if the rewrite didn't land.
# We intentionally do NOT set -p:StaticWebAssetBasePath at publish time:
# the current .NET 10 SDK does not use it to rewrite <base href> for BWA,
# so relying on it would silently fail with no signal in the logs.
- name: Rewrite base href
env:
REPO_NAME: ${{ github.event.repository.name }}
run: |
echo "Setting <base href> to /${REPO_NAME}/"
sed -Ei "s|<base[[:space:]]+href=\"/\"[[:space:]]*/?>|<base href=\"/${REPO_NAME}/\" />|g" publish/wwwroot/index.html
- name: Verify base href rewrite
env:
REPO_NAME: ${{ github.event.repository.name }}
run: |
if ! grep -q "<base href=\"/${REPO_NAME}/\"" publish/wwwroot/index.html; then
echo "::error::<base href> was not rewritten to /${REPO_NAME}/. The app would 404 in production."
echo "Found the following <base> line(s):"
grep -n '<base ' publish/wwwroot/index.html || true
exit 1
fi
# index.html's CSP deliberately has no script-src 'unsafe-inline' (an XSS in
# the Markdown preview would otherwise be a straight path to the PAT in
# localStorage). But `dotnet publish` rewrites the empty
# <script type="importmap"></script> placeholder into a populated inline
# import map whose body embeds fingerprinted filenames — so its hash changes
# on every build and cannot be hardcoded in the source HTML. Without a
# matching hash the browser blocks the import map, blazor.webassembly.js then
# requests the unfingerprinted `_framework/dotnet.js` (404), and the app hangs
# on the loading spinner forever with nothing rendered in the page. Recompute
# and pin it here, against the published file, after the base-href rewrite.
- name: Pin import-map CSP hash
run: python3 .github/scripts/pin_importmap_csp.py publish/wwwroot/index.html
# Both greps are scoped to the CSP <meta> line on purpose. index.html also
# contains a prose comment *about* the CSP that mentions both "script-src" and
# "'unsafe-inline'", so an unscoped grep over the whole file matches the
# documentation and fails (or passes) for entirely the wrong reason.
- name: Verify CSP hardening survived the build
run: |
csp="$(grep 'http-equiv="Content-Security-Policy"' publish/wwwroot/index.html || true)"
if [ -z "$csp" ]; then
echo "::error::No CSP meta tag in the published index.html."
exit 1
fi
script_src="$(printf '%s' "$csp" | grep -o "script-src[^;]*" || true)"
echo "script-src: $script_src"
case "$script_src" in
*"'sha256-"*) ;;
*) echo "::error::script-src carries no import-map hash — the app would not boot."; exit 1 ;;
esac
case "$script_src" in
*"'unsafe-inline'"*)
echo "::error::script-src regained 'unsafe-inline'. An injected inline script would execute, and the GitHub PAT in localStorage is one line of JS away. Refusing to deploy."
exit 1 ;;
esac
img_src="$(printf '%s' "$csp" | grep -o "img-src[^;]*" || true)"
echo "img-src: $img_src"
# A bare `https:` source re-opens the image-beacon exfil channel that
# connect-src cannot see. Compare token by token: `https:` on its own is
# the wildcard and must fail, while `https://host` is an explicit host and
# is fine. Substring matching cannot tell those two apart.
for src in $img_src; do
if [ "$src" = "https:" ] || [ "$src" = "http:" ] || [ "$src" = "*" ]; then
echo "::error::img-src contains the wildcard '$src' — a compromised renderer could beacon the PAT to any host (img-src is not covered by connect-src). Refusing to deploy."
exit 1
fi
done
# Blazor ships filenames starting with '_' (e.g. _framework). GitHub
# Pages runs Jekyll by default, which strips them. .nojekyll disables it.
- name: Add .nojekyll
run: touch publish/wwwroot/.nojekyll
# SPA fallback so client-side routes (e.g. deep links) don't 404.
- name: Add SPA 404 fallback
run: cp publish/wwwroot/index.html publish/wwwroot/404.html
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: publish/wwwroot
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4