Skip to content

Commit d0899b5

Browse files
authored
feat(ZMSKVR): implement php code scanning with psalm (#2203)
* chore(ZMSKVR): Add Psalm security scan workflow for php code scan * fix(ZMSKVR): Refactor Psalm security scan workflow * feat(ZMSKVR): add psalm security scan in zmscitizenapi * feat(ZMSKVR): add psalm security scan in zmscitizenapi * fix(ZMSKVR): output redirection for Psalm results * feat(ZMSKVR): add psalm security scan in zmscitizenapi add psalm.xml * fix(ZMSKVR): Update psalm.yml * fix(ZMSKVR): always upload Psalm SARIF results Ensure the Psalm workflow uploads SARIF findings even when Psalm returns a non-zero exit code, then fail the job explicitly afterward so CI enforcement remains intact. * fix(ZMSKVR): align SARIF checkout path for Psalm Set upload-sarif checkout_path to the matrix project directory so GitHub can resolve Psalm paths and show code previews for alerts. * fix(ZMSKVR): use relative checkout_path for SARIF upload Switch upload-sarif checkout_path to the matrix project relative path so GitHub can resolve Psalm file locations correctly. * fix(ZMSKVR): trying something * fix(ZMSKVR): trying something * fix(ZMSKVR): Psalm Security Scan add manual trigger and fix preview * fix(ZMSKVR): normalize Psalm SARIF paths before upload Rewrite SARIF artifact URIs to repository-root-relative paths so GitHub code scanning can resolve files and render alert previews. * feat(ZMSKVR): roll out Psalm config across all modules Add Psalm as a dev dependency in each module, create per-module psalm.xml configs, and expand the Psalm CI matrix to scan all modules with module-aware SARIF path normalization. * fix(ZMSKVR): guard SARIF upload on generated report Upload SARIF only when the module report exists and fail with a clear message when Psalm or SARIF generation fails. * fix(ZMSKVR): disable fail-fast and refresh module lockfiles Prevent matrix runs from canceling sibling jobs and commit regenerated composer.lock files after adding Psalm to all modules. * feat(ZMSKVR): split Psalm dead-code scanning strategy Disable dead-code checks in per-module scans to reduce alert noise and add an opt-in monorepo dead-code job that analyzes cross-module usage. * feat(ZMSKVR): run monorepo dead-code job on all triggers Execute the psalm-dead-code job alongside the matrix scan for push, pull request, scheduled, and manual workflow runs. * fix(ZMSKVR): include module runtime context in Psalm configs Add config/bootstrap/routing files to module and monorepo Psalm project files so global App symbols are available during analysis. * feat(ZMSKVR): Add emojis to workflow psalm.yml and set schedule * feat(ZMSKVR): Add emojis to CodeQL Advanced name * fix(ZMSKVR): align Psalm workflow with CodeQL triggers and concurrency Match CodeQL Advanced on/pull_request and push to main, add pull-requests read permission, and cancel in-progress runs per ref. * fix(ZMSKVR): align DDEV MariaDB 10.11 for RENAME COLUMN migrations * fix(ZMSKVR): map Zmsclient RequestException to client communication error Handle BO\Zmsclient\Psr7\RequestException (e.g. SSL/cURL failures) so responses use zmsClientCommunicationError instead of unknownError. Align ErrorMessages::get call with single-parameter signature. * fix(ZMSKVR): default ZMS_API_URL to loopback HTTP in DDEV template PHP cURL to https://*.ddev.site from the web container often fails TLS verification; same Apache is reachable over http://127.0.0.1 without SSL. * clean(ZMSKVR): Changed file names detected by Psalm PHP Security Scan * fix(ZMSKVR): fix monorepo Psalm config vendor path resolution Remove root vendor ignore from psalm.monorepo.xml; with resolveFromConfigFile it pointed at missing ./vendor and broke CI. * clean(ZMSKVR): mark Slim Sanitizer as final Satisfy Psalm ClassMustBeFinal; class is not extended and only exposes static helpers. * fix(ZMSKVR): unused var issue * fix(ZMSKVR): fail Psalm CI only when SARIF is missing, not on findings Psalm exits non-zero when it reports issues but still writes SARIF for CodeQL upload. Treat missing SARIF as the job failure condition instead. * fix(ZMSKVR): fail Psalm CI when findings exist, still upload SARIF Use continue-on-error on the scan step so SARIF normalize/upload run even when Psalm exits non-zero, then fail the job if SARIF is missing or the recorded Psalm exit code is not zero. * feat(ZMS): run Psalm scan on main schedule only while debt remains Drop pull_request and push triggers so branch CI is not blocked by existing findings; keep daily 05:00 UTC uploads on main with fail-on-error unchanged. * fix(ZMSKVR): pin vimeo/psalm to 6.5.0 for CI PHP 8.3.6 compatibility Psalm 6.16+ requires patched PHP 8.3.16+, which breaks reference-libraries on GitHub runners; 6.5.0 is the last release accepting PHP 8.3.6. * fix(ZMSKVR): exclude vendor dirs from monorepo Psalm scan Prevent the dead-code job from analyzing third-party dependencies across all 13 modules, matching per-module psalm.xml ignoreFiles behavior.
1 parent b622439 commit d0899b5

47 files changed

Lines changed: 40903 additions & 10642 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.ddev/.env.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# ZMS Core
2-
ZMS_API_URL=https://zms.ddev.site/terminvereinbarung/api/2
2+
ZMS_API_URL=http://127.0.0.1/terminvereinbarung/api/2
33
ZMS_CRONROOT=1
44
ZMS_ENV=dev
55
#ZMS_TIMEADJUST='2016-04-01 H:i'

.ddev/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ additional_fqdns:
99
- keycloak
1010
database:
1111
type: mariadb
12-
version: "10.4"
12+
version: "10.11"
1313
use_dns_when_possible: true
1414
composer_version: "2"
1515
web_environment:

.github/workflows/codeql.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Workflow for advanced CodeQL setup used for scanning Java/JavaScript/TypeScript/Vue/Python based source files
2-
name: 🛡️ CodeQL Advanced
2+
name: 🛡️ ☕ 📜 🐍 CodeQL Advanced
33
env:
44
# Whether to analyze Java code or not (only set to true if repo has Java source code)
55
analyze-java: true

.github/workflows/psalm.yml

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
name: 🛡️ 🐘 Psalm PHP Security Scan
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
run_dead_code:
7+
description: "Run monorepo dead-code scan"
8+
required: false
9+
default: true
10+
type: boolean
11+
# Scheduled on main only until ~18k Psalm findings are cleared (PR/push would fail every CI).
12+
# Re-enable pull_request / push when errorLevel is stricter or debt is baselined.
13+
schedule:
14+
- cron: '0 5 * * *' # 05:00 UTC daily (default branch: main)
15+
16+
permissions:
17+
contents: read
18+
pull-requests: read
19+
security-events: write
20+
21+
concurrency:
22+
group: ${{ github.workflow }}-${{ github.ref }}
23+
cancel-in-progress: true
24+
25+
jobs:
26+
psalm:
27+
runs-on: ubuntu-latest
28+
if: github.ref == 'refs/heads/main'
29+
30+
strategy:
31+
fail-fast: false
32+
matrix:
33+
project:
34+
- mellon
35+
- zmsadmin
36+
- zmsapi
37+
- zmscalldisplay
38+
- zmscitizenapi
39+
- zmsclient
40+
- zmsdb
41+
- zmsdldb
42+
- zmsentities
43+
- zmsmessaging
44+
- zmsslim
45+
- zmsstatistic
46+
- zmsticketprinter
47+
48+
steps:
49+
- name: Checkout repository (with submodules)
50+
uses: actions/checkout@v4
51+
with:
52+
submodules: true
53+
54+
- name: Setup PHP
55+
uses: shivammathur/setup-php@v2
56+
with:
57+
php-version: 8.3
58+
tools: composer
59+
coverage: none
60+
61+
- name: Get Composer cache directory
62+
id: composer-cache
63+
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
64+
working-directory: ${{ matrix.project }}
65+
66+
- name: Cache Composer dependencies
67+
uses: actions/cache@v4
68+
with:
69+
path: ${{ steps.composer-cache.outputs.dir }}
70+
key: ${{ runner.os }}-composer-${{ matrix.project }}-${{ hashFiles(format('{0}/composer.lock', matrix.project)) }}
71+
restore-keys: |
72+
${{ runner.os }}-composer-${{ matrix.project }}-
73+
74+
- name: Install dependencies
75+
working-directory: ${{ matrix.project }}
76+
run: composer install --no-progress --prefer-dist --no-interaction
77+
78+
- name: Run Psalm Security Scan
79+
id: psalm
80+
continue-on-error: true
81+
working-directory: ${{ matrix.project }}
82+
run: |
83+
set +e
84+
vendor/bin/psalm \
85+
--no-progress \
86+
--report=../results-${{ matrix.project }}.sarif
87+
psalm_exit=$?
88+
set -e
89+
echo "exit_code=${psalm_exit}" >> "$GITHUB_OUTPUT"
90+
if [ ! -f "../results-${{ matrix.project }}.sarif" ]; then
91+
echo "Psalm did not produce SARIF for ${{ matrix.project }} (exit code: ${psalm_exit})."
92+
exit 1
93+
fi
94+
if [ "${psalm_exit}" -ne 0 ]; then
95+
echo "Psalm reported issues for ${{ matrix.project }} (exit code: ${psalm_exit})."
96+
exit "${psalm_exit}"
97+
fi
98+
99+
- name: Normalize SARIF paths to repository root
100+
env:
101+
PROJECT: ${{ matrix.project }}
102+
run: |
103+
python - <<'PY'
104+
import os
105+
import json
106+
107+
project = os.environ["PROJECT"]
108+
sarif_path = f"results-{project}.sarif"
109+
110+
with open(sarif_path, "r", encoding="utf-8") as file:
111+
sarif = json.load(file)
112+
113+
for run in sarif.get("runs", []):
114+
for result in run.get("results", []):
115+
for location in result.get("locations", []):
116+
artifact = (
117+
location.get("physicalLocation", {})
118+
.get("artifactLocation", {})
119+
)
120+
uri = artifact.get("uri")
121+
if isinstance(uri, str) and not uri.startswith(f"{project}/"):
122+
artifact["uri"] = f"{project}/{uri}"
123+
124+
with open(sarif_path, "w", encoding="utf-8") as file:
125+
json.dump(sarif, file)
126+
PY
127+
128+
- name: Check SARIF file exists
129+
id: sarif
130+
run: |
131+
if [ -f "results-${{ matrix.project }}.sarif" ]; then
132+
echo "exists=true" >> "$GITHUB_OUTPUT"
133+
else
134+
echo "exists=false" >> "$GITHUB_OUTPUT"
135+
fi
136+
137+
- name: Upload SARIF results
138+
if: always() && steps.sarif.outputs.exists == 'true'
139+
uses: github/codeql-action/upload-sarif@v3
140+
with:
141+
sarif_file: results-${{ matrix.project }}.sarif
142+
checkout_path: ${{ matrix.project }}
143+
144+
- name: Fail job if Psalm scan or SARIF generation failed
145+
if: always()
146+
run: |
147+
if [ "${{ steps.sarif.outputs.exists }}" != "true" ]; then
148+
echo "Missing SARIF output for ${{ matrix.project }} (results-${{ matrix.project }}.sarif)."
149+
exit 1
150+
fi
151+
psalm_exit="${{ steps.psalm.outputs.exit_code }}"
152+
if [ -z "${psalm_exit}" ] || [ "${psalm_exit}" != "0" ]; then
153+
echo "Psalm failed for ${{ matrix.project }} (exit code: ${psalm_exit:-unknown})."
154+
exit 1
155+
fi
156+
157+
psalm-dead-code:
158+
runs-on: ubuntu-latest
159+
if: github.ref == 'refs/heads/main' && (github.event_name == 'schedule' || inputs.run_dead_code == true)
160+
161+
steps:
162+
- name: Checkout repository (with submodules)
163+
uses: actions/checkout@v4
164+
with:
165+
submodules: true
166+
167+
- name: Setup PHP
168+
uses: shivammathur/setup-php@v2
169+
with:
170+
php-version: 8.3
171+
tools: composer
172+
coverage: none
173+
174+
- name: Get Composer cache directory
175+
id: composer-cache-monorepo
176+
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
177+
working-directory: zmsapi
178+
179+
- name: Cache Composer dependencies
180+
uses: actions/cache@v4
181+
with:
182+
path: ${{ steps.composer-cache-monorepo.outputs.dir }}
183+
key: ${{ runner.os }}-composer-psalm-monorepo-${{ hashFiles('zmsapi/composer.lock') }}
184+
restore-keys: |
185+
${{ runner.os }}-composer-psalm-monorepo-
186+
187+
- name: Install dependencies
188+
working-directory: zmsapi
189+
run: composer install --no-progress --prefer-dist --no-interaction
190+
191+
- name: Run Psalm dead-code scan (monorepo)
192+
id: psalm_dead_code
193+
continue-on-error: true
194+
run: |
195+
set +e
196+
zmsapi/vendor/bin/psalm \
197+
-c psalm.monorepo.xml \
198+
--no-progress \
199+
--report=results-monorepo.sarif
200+
psalm_exit=$?
201+
set -e
202+
echo "exit_code=${psalm_exit}" >> "$GITHUB_OUTPUT"
203+
if [ ! -f "results-monorepo.sarif" ]; then
204+
echo "Psalm did not produce monorepo SARIF (exit code: ${psalm_exit})."
205+
exit 1
206+
fi
207+
if [ "${psalm_exit}" -ne 0 ]; then
208+
echo "Psalm monorepo scan reported issues (exit code: ${psalm_exit})."
209+
exit "${psalm_exit}"
210+
fi
211+
212+
- name: Check monorepo SARIF file exists
213+
id: sarif_monorepo
214+
run: |
215+
if [ -f "results-monorepo.sarif" ]; then
216+
echo "exists=true" >> "$GITHUB_OUTPUT"
217+
else
218+
echo "exists=false" >> "$GITHUB_OUTPUT"
219+
fi
220+
221+
- name: Upload monorepo SARIF results
222+
if: always() && steps.sarif_monorepo.outputs.exists == 'true'
223+
uses: github/codeql-action/upload-sarif@v3
224+
with:
225+
sarif_file: results-monorepo.sarif
226+
227+
- name: Fail job if monorepo Psalm scan or SARIF generation failed
228+
if: always()
229+
run: |
230+
if [ "${{ steps.sarif_monorepo.outputs.exists }}" != "true" ]; then
231+
echo "Missing monorepo SARIF output (results-monorepo.sarif)."
232+
exit 1
233+
fi
234+
psalm_exit="${{ steps.psalm_dead_code.outputs.exit_code }}"
235+
if [ -z "${psalm_exit}" ] || [ "${psalm_exit}" != "0" ]; then
236+
echo "Psalm monorepo scan failed (exit code: ${psalm_exit:-unknown})."
237+
exit 1
238+
fi

mellon/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
}
1010
],
1111
"require-dev": {
12+
"vimeo/psalm": "6.5.0",
1213
"phpmd/phpmd": "^2.8.0",
1314
"squizlabs/php_codesniffer": "*",
1415
"phpunit/phpunit": "^11",

0 commit comments

Comments
 (0)