Skip to content

Commit 44b3e32

Browse files
test(server): make security mutation analysis reliable (#1544)
1 parent f2176f6 commit 44b3e32

28 files changed

Lines changed: 753 additions & 623 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hephaestus": patch
3+
---
4+
5+
OAuth return targets now reject excessive percent-encoding, and OAuth intent cookies enforce expiration and future timestamps at millisecond precision.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: Security mutation testing
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
paths:
7+
- ".github/workflows/security-mutation.yml"
8+
- ".github/actions/setup-pnpm-node/**"
9+
- ".node-version"
10+
- "package.json"
11+
- "pnpm-lock.yaml"
12+
- "scripts/run-security-mutations.ts"
13+
- "scripts/run-security-mutations.test.ts"
14+
- "server/application/pom.xml"
15+
- "server/generated-clients/**"
16+
- "server/.mvn/**"
17+
- "server/mvnw"
18+
- "server/pom.xml"
19+
- "server/application/src/**/java/de/tum/cit/aet/hephaestus/core/auth/oauth/**"
20+
- "server/application/src/**/java/de/tum/cit/aet/hephaestus/core/security/**"
21+
- "server/application/src/**/java/de/tum/cit/aet/hephaestus/integration/core/connection/**"
22+
- "server/application/src/**/java/de/tum/cit/aet/hephaestus/integration/core/oauth/state/**"
23+
- "server/application/src/**/java/de/tum/cit/aet/hephaestus/integration/scm/gitlab/webhook/**"
24+
25+
permissions:
26+
contents: read
27+
28+
concurrency:
29+
group: security-mutation-${{ github.ref }}
30+
cancel-in-progress: true
31+
32+
jobs:
33+
pitest:
34+
name: Security mutation suite
35+
runs-on: ubuntu-latest
36+
timeout-minutes: 10
37+
steps:
38+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
39+
with:
40+
persist-credentials: false
41+
42+
- uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
43+
with:
44+
distribution: temurin
45+
java-version: "21"
46+
cache: maven
47+
cache-dependency-path: "server/**/pom.xml"
48+
49+
- uses: ./.github/actions/setup-pnpm-node
50+
51+
- name: Install repository tooling
52+
run: pnpm install --frozen-lockfile
53+
54+
- name: Run security mutation suite
55+
run: timeout --kill-after=30s 8m pnpm run test:server:mutation
56+
57+
- name: Upload mutation report
58+
if: always()
59+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
60+
with:
61+
name: security-mutation-report-${{ github.run_id }}-${{ github.run_attempt }}
62+
path: server/application/target/pit-reports
63+
if-no-files-found: error
64+
retention-days: 30

docs/auth-architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ Login `ClientRegistration`s are built from the `login_provider` store by
112112
`LoginProviderClientRegistrationRepository` in `core.auth.provider` (it depends only on the store +
113113
Spring Security — integration may not reach into `core.auth.provider`). The integration side keeps
114114
`RegistrationToGitProviderResolver` (maps a registration to its provider record via the
115-
`GitProviderRegistry` SPI) and the reusable `IssuerDiscoveryProbe`; `ImpersonationGuard` lives in
115+
`GitProviderRegistry` SPI); `ImpersonationGuard` lives in
116116
`core.security`.
117117

118118
Boundaries: `workspace` / `integration.scm` / `notification` depend on `core.auth` (read model +
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
sidebar_position: 9
3+
title: Security mutation testing
4+
---
5+
6+
# Security mutation testing
7+
8+
PIT mutates a curated set of security-boundary classes and runs their focused unit tests. The
9+
advisory result measures whether those tests reject injected changes; it is not a security guarantee
10+
or a mutation-score gate.
11+
12+
## Run the suite
13+
14+
The **Security mutation testing** workflow runs when a pull request changes an affected security
15+
area or the suite's build inputs. It also supports manual dispatch. Run the same analysis locally
16+
with JDK 21:
17+
18+
```bash
19+
pnpm run test:server:mutation
20+
```
21+
22+
The command prepares reactor dependencies, compiles the target tests, and runs PIT. It fails if any
23+
Maven phase fails, the report is missing or invalid, or PIT leaves a mutation in a technical or
24+
incomplete state. The job summary reports timing and outcomes. PIT's HTML/XML reports and a Markdown
25+
summary are written below the application module's `target/pit-reports` directory and uploaded by
26+
the workflow even on failure.
27+
28+
The command evaluates the full target set without incremental analysis and is limited to eight
29+
minutes, leaving two minutes for artifact upload.
30+
31+
## Triage
32+
33+
The pull-request author reviews reported survivors and uncovered mutations in affected code. The
34+
affected server CODEOWNERS review any accepted classification.
35+
36+
- Add a public-behavior test when a mutant exposes an unverified contract.
37+
- Explain an accepted equivalent or unproductive mutant in the pull-request description instead of
38+
asserting private call order or implementation details.
39+
- Remove unwired code instead of building mutation tests around it.
40+
- Treat technical and incomplete statuses as an invalid run, never as killed mutations.
41+
42+
[Issue #1498](https://github.com/ls1intum/Hephaestus/issues/1498) records the suite's evaluation and
43+
the decision to keep it non-required and advisory.
44+
45+
## Further reading
46+
47+
- [PIT Maven guide](https://pitest.org/quickstart/maven/)
48+
- [Mutation testing at Google](https://testing.googleblog.com/2021/04/mutation-testing.html)

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"test:server:architecture": "cd server && ./mvnw -pl application -am test -Parchitecture-tests --batch-mode",
2929
"test:server:verification": "cd server && ./mvnw -pl application -am test -Parchitecture-tests -Dsurefire.includedGroups=unit,architecture -DskipCoverage=false --batch-mode",
3030
"test:server:integration": "cd server && ./mvnw -pl application -am test -Dsurefire.includedGroups=integration -Dparallel=none --batch-mode",
31+
"test:server:mutation": "node --import tsx scripts/run-security-mutations.ts",
3132
"summarize:test-results": "node scripts/summarize-test-results.ts",
3233
"build:webapp": "pnpm --filter webapp run build",
3334
"nats:extract-examples": "node --import tsx scripts/nats-extract-examples.ts",
@@ -42,7 +43,7 @@
4243
"format:config:check": "oxfmt --check *.ts *.json .oxfmtrc.json .oxlintrc.json .changeset/*.cjs .changeset/*.json .vscode/*.json scripts/tsconfig.json project.code-workspace",
4344
"lint:agents": "oxlint server docker scripts docs .changeset .github.qkg1.topmitlint.config.ts",
4445
"lint:agents:fix": "oxlint --fix server docker scripts docs .changeset .github.qkg1.topmitlint.config.ts",
45-
"check:agents": "pnpm run format:agents:check && pnpm run format:docs:check && pnpm run format:config:check && pnpm run lint:agents && pnpm run typecheck:agents && pnpm run typecheck:scripts",
46+
"check:agents": "pnpm run format:agents:check && pnpm run format:docs:check && pnpm run format:config:check && pnpm run lint:agents && pnpm run typecheck:agents && pnpm run typecheck:scripts && node --test scripts/run-security-mutations.test.ts",
4647
"check:agents:fix": "pnpm run format:agents && pnpm run format:docs && pnpm run format:config && oxlint --fix server docker scripts docs .changeset .github.qkg1.topmitlint.config.ts",
4748
"ci:agents": "pnpm run format:agents:check && pnpm run format:docs:check && pnpm run format:config:check && oxlint -f github server docker scripts docs .changeset .github.qkg1.topmitlint.config.ts",
4849
"format:server": "pnpm run format:java",
@@ -104,6 +105,7 @@
104105
"ajv-formats": "3.0.1",
105106
"commander": "14.0.3",
106107
"fast-xml-parser": "^5.11.0",
108+
"fast-xml-validator": "1.4.2",
107109
"graphql": "16.14.0",
108110
"husky": "9.1.7",
109111
"jsdom": "29.1.1",

pnpm-lock.yaml

Lines changed: 59 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/check-java-nullness.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env node
22
import { execFile } from "node:child_process";
3-
import { readFile } from "node:fs/promises";
3+
import { access, readFile } from "node:fs/promises";
44
import { resolve } from "node:path";
55
import { promisify } from "node:util";
66

@@ -33,7 +33,19 @@ export async function discoverJavaSourcePaths(
3333
},
3434
},
3535
);
36-
const paths = stdout.split("\0").filter(isHandwrittenJavaSource);
36+
const candidates = stdout.split("\0").filter(isHandwrittenJavaSource);
37+
const present = await Promise.all(
38+
candidates.map(async (path) => {
39+
try {
40+
await access(resolve(root, path));
41+
return path;
42+
} catch (error) {
43+
if (isNodeError(error) && error.code === "ENOENT") return undefined;
44+
throw error;
45+
}
46+
}),
47+
);
48+
const paths = present.filter((path): path is string => path !== undefined);
3749
if (paths.length === 0) {
3850
throw new Error(
3951
"No handwritten Java sources found under server/application/src/{main,test}/java; refusing to pass without checking anything.",
@@ -42,6 +54,10 @@ export async function discoverJavaSourcePaths(
4254
return paths;
4355
}
4456

57+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
58+
return error instanceof Error;
59+
}
60+
4561
function unicodeEscapes(source: string): string {
4662
return source.replace(/\\u+([0-9a-fA-F]{4})/g, (_, hex: string) =>
4763
String.fromCharCode(Number.parseInt(hex, 16)),

0 commit comments

Comments
 (0)