From 151c1de9abd94bf18640286d7f9adc53b02c9199 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Fri, 26 Jun 2026 10:44:16 +0100 Subject: [PATCH 1/6] moved from codacy to semgrep --- .codacy.yml | 41 ------------------- .deepsource.toml | 17 -------- .github/workflows/codacy.yml | 26 ------------ .github/workflows/codeql.yml | 33 +++++++++++++++ .github/workflows/semgrep.yml | 36 ++++++++++++++++ ...EstateManagementUI.IntegrationTests.csproj | 2 +- 6 files changed, 70 insertions(+), 85 deletions(-) delete mode 100644 .codacy.yml delete mode 100644 .deepsource.toml delete mode 100644 .github/workflows/codacy.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/semgrep.yml diff --git a/.codacy.yml b/.codacy.yml deleted file mode 100644 index 913fd99d..00000000 --- a/.codacy.yml +++ /dev/null @@ -1,41 +0,0 @@ -exclude_paths: - # Ignore the test projects entirely - - EstateManagementUI.BlazorServer.Tests/** - - EstateManagementUI.IntegrationTests/** - - # Common build / IDE artifacts - - bin/** - - obj/** - - .vs/** - - .vscode/** - - .idea/** - - packages/** - - # Node / frontend artifacts - - node_modules/** - - wwwroot/lib/** - - # Generated code and designer files - - "**/*.g.cs" - - "**/*.g.i.cs" - - "**/*.designer.cs" - - "**/*.Generated.cs" - - "**/Generated/**" - - # Coverage and test results - - coverage/** - - coverage.* - - coverage.xml - - lcov.info - - TestResults/** - - # Archives / legacy folders - - Archive/** - - "**/Archive/**" - - # Other artifacts - - "*.user" - - "*.suo" - - "*.db" - - "*.sqlite" - - "*StubTestData.cs" diff --git a/.deepsource.toml b/.deepsource.toml deleted file mode 100644 index c3730ebf..00000000 --- a/.deepsource.toml +++ /dev/null @@ -1,17 +0,0 @@ -version = 1 - -[[analyzers]] -name = "csharp" -enabled = true - -[[analyzers]] -name = "docker" -enabled = true - -[[analyzers]] -name = "javascript" -enabled = true - -[[analyzers]] -name = "sql" -enabled = true diff --git a/.github/workflows/codacy.yml b/.github/workflows/codacy.yml deleted file mode 100644 index 6998ec39..00000000 --- a/.github/workflows/codacy.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Codacy Issue Sync - -on: - push: - branches: [main, master] - workflow_dispatch: - -permissions: - contents: read - issues: write - -jobs: - codacy-issue-sync: - uses: TransactionProcessing/org-ci-workflows/.github/workflows/sync-codacy-issues.yml@main - secrets: inherit - with: - # Optional: you can omit both and the reusable workflow will default them - # to the calling repo owner/name. Keeping them explicit is fine too. - codacy_org: TransactionProcessing - codacy_repo: EstateManagementUI - - # Optional knobs - dry_run: false - severity_filter: "Error,High,Warning,Info" - extra_labels: "" - assignees: "" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..bdfff4c0 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,33 @@ +name: CodeQL + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "30 2 * * 1" + +permissions: + contents: read + security-events: write + +jobs: + analyze: + name: Analyze C# + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: csharp + build-mode: none + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 00000000..5eb25842 --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,36 @@ +name: Semgrep + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "15 3 * * 1" + +permissions: + contents: read + security-events: write + +jobs: + scan: + name: Semgrep Scan + runs-on: ubuntu-latest + + container: + image: semgrep/semgrep:latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Semgrep + run: semgrep scan --config auto --sarif --output semgrep.sarif + + - name: Upload SARIF to Code Scanning + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: semgrep.sarif + category: semgrep diff --git a/EstateManagementUI.IntegrationTests/EstateManagementUI.IntegrationTests.csproj b/EstateManagementUI.IntegrationTests/EstateManagementUI.IntegrationTests.csproj index 5f695b9a..0da15770 100644 --- a/EstateManagementUI.IntegrationTests/EstateManagementUI.IntegrationTests.csproj +++ b/EstateManagementUI.IntegrationTests/EstateManagementUI.IntegrationTests.csproj @@ -21,7 +21,7 @@ - + e to semgrep all From f9e66cd6f19e620a396ad73c085a086cf11f86d2 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Fri, 26 Jun 2026 11:11:27 +0100 Subject: [PATCH 2/6] morw workflow changes --- .github/workflows/codecoverage.yml | 30 ++++++++++++++----------- .github/workflows/semgrep.yml | 36 ------------------------------ 2 files changed, 17 insertions(+), 49 deletions(-) delete mode 100644 .github/workflows/semgrep.yml diff --git a/.github/workflows/codecoverage.yml b/.github/workflows/codecoverage.yml index 736d7475..a3717062 100644 --- a/.github/workflows/codecoverage.yml +++ b/.github/workflows/codecoverage.yml @@ -5,8 +5,15 @@ on: # branches to consider in the event; optional, defaults to all branches: - main + pull_request: + branches: + - main workflow_dispatch: +permissions: + contents: read + code-quality: write + jobs: codecoverage: name: "Code Coverage" @@ -16,7 +23,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.3.4 + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Restore Nuget Packages run: dotnet restore EstateManagementUI.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }} @@ -27,18 +36,13 @@ jobs: - name: Run Unit Tests run: | echo "ASPNETCORE_ENVIRONMENT are > ${ASPNETCORE_ENVIRONMENT}" - dotnet test EstateManagementUI.BlazorServer.Tests/EstateManagementUI.BlazorServer.Tests.csproj --settings .runsettings /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:ExcludeByAttribute="Obsolete" /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" /p:CoverletOutput="../lcov1.info" /maxcpucount:1 /p:CoverletOutputFormat="lcov" - - - name: Install LCOV merger - run: npm install -g lcov-result-merger - - - name: Merge LCOV reports - run: | mkdir -p coverage - lcov-result-merger "*.info" > lcov.info + dotnet test EstateManagementUI.BlazorServer.Tests/EstateManagementUI.BlazorServer.Tests.csproj --settings .runsettings /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:ExcludeByAttribute="Obsolete" /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" /p:CoverletOutput="coverage/coverage" /maxcpucount:1 /p:CoverletOutputFormat="cobertura" - - name: Upload merged coverage to Codacy - uses: codacy/codacy-coverage-reporter-action@v1 + - name: Upload coverage report + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + uses: actions/upload-code-coverage@v1 with: - project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} - coverage-reports: ./lcov.info + file: coverage/coverage.cobertura.xml + language: C# + label: code-coverage/blazor diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml deleted file mode 100644 index 5eb25842..00000000 --- a/.github/workflows/semgrep.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Semgrep - -on: - push: - branches: - - main - pull_request: - branches: - - main - schedule: - - cron: "15 3 * * 1" - -permissions: - contents: read - security-events: write - -jobs: - scan: - name: Semgrep Scan - runs-on: ubuntu-latest - - container: - image: semgrep/semgrep:latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Run Semgrep - run: semgrep scan --config auto --sarif --output semgrep.sarif - - - name: Upload SARIF to Code Scanning - uses: github/codeql-action/upload-sarif@v4 - with: - sarif_file: semgrep.sarif - category: semgrep From 6ef3d1fcf28387a45b4bcee08f549e50a1388f58 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Fri, 26 Jun 2026 11:16:51 +0100 Subject: [PATCH 3/6] .. --- .../EstateManagementUI.IntegrationTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EstateManagementUI.IntegrationTests/EstateManagementUI.IntegrationTests.csproj b/EstateManagementUI.IntegrationTests/EstateManagementUI.IntegrationTests.csproj index 0da15770..5f695b9a 100644 --- a/EstateManagementUI.IntegrationTests/EstateManagementUI.IntegrationTests.csproj +++ b/EstateManagementUI.IntegrationTests/EstateManagementUI.IntegrationTests.csproj @@ -21,7 +21,7 @@ - e to semgrep + all From 36e1a532929b193a5cd3ea67e3b0a6cc5e738942 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Fri, 26 Jun 2026 11:24:11 +0100 Subject: [PATCH 4/6] ... --- .github/workflows/codecoverage.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codecoverage.yml b/.github/workflows/codecoverage.yml index a3717062..6f764a5c 100644 --- a/.github/workflows/codecoverage.yml +++ b/.github/workflows/codecoverage.yml @@ -36,13 +36,13 @@ jobs: - name: Run Unit Tests run: | echo "ASPNETCORE_ENVIRONMENT are > ${ASPNETCORE_ENVIRONMENT}" - mkdir -p coverage - dotnet test EstateManagementUI.BlazorServer.Tests/EstateManagementUI.BlazorServer.Tests.csproj --settings .runsettings /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:ExcludeByAttribute="Obsolete" /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" /p:CoverletOutput="coverage/coverage" /maxcpucount:1 /p:CoverletOutputFormat="cobertura" + mkdir -p "$GITHUB_WORKSPACE/coverage" + dotnet test EstateManagementUI.BlazorServer.Tests/EstateManagementUI.BlazorServer.Tests.csproj --settings .runsettings /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:ExcludeByAttribute="Obsolete" /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" /p:CoverletOutput="$GITHUB_WORKSPACE/coverage/coverage" /maxcpucount:1 /p:CoverletOutputFormat="cobertura" - name: Upload coverage report if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository uses: actions/upload-code-coverage@v1 with: - file: coverage/coverage.cobertura.xml + file: ${{ github.workspace }}/coverage/coverage.cobertura.xml language: C# label: code-coverage/blazor From 73b538ff7ef3455a444116665a7314d5777326b5 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Fri, 26 Jun 2026 11:34:16 +0100 Subject: [PATCH 5/6] .. --- .github/workflows/codeql.yml | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index bdfff4c0..00000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: CodeQL - -on: - push: - branches: - - main - pull_request: - branches: - - main - schedule: - - cron: "30 2 * * 1" - -permissions: - contents: read - security-events: write - -jobs: - analyze: - name: Analyze C# - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v4 - with: - languages: csharp - build-mode: none - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 From 1cd03261385959afc033255267615ce7eaf48e35 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Fri, 26 Jun 2026 12:05:43 +0100 Subject: [PATCH 6/6] Add Semgrep scan and issue sync --- .github/workflows/semgrep.yml | 49 ++++++ .semgrepignore | 19 +++ scripts/semgrep-sync-issues.mjs | 256 ++++++++++++++++++++++++++++++++ 3 files changed, 324 insertions(+) create mode 100644 .github/workflows/semgrep.yml create mode 100644 .semgrepignore create mode 100644 scripts/semgrep-sync-issues.mjs diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 00000000..2f263dba --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,49 @@ +name: Semgrep + +on: + push: + branches: + - main + schedule: + - cron: "0 3 * * *" + workflow_dispatch: + inputs: + min_severity: + description: "Minimum severity for GitHub issues" + required: false + default: warning + type: choice + options: + - warning + - error + +permissions: + contents: read + issues: write + +jobs: + scan: + name: Scan and Sync + runs-on: ubuntu-latest + + env: + SEMGREP_MIN_SEVERITY: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.min_severity || vars.SEMGREP_MIN_SEVERITY || 'warning' }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Semgrep + run: | + set +e + docker run --rm -v "$GITHUB_WORKSPACE:/src" -w /src semgrep/semgrep:latest semgrep scan --config auto --json --output /src/semgrep.json /src + code=$? + set -e + if [ "$code" -gt 1 ]; then + exit "$code" + fi + + - name: Sync Semgrep findings to GitHub issues + env: + GITHUB_TOKEN: ${{ github.token }} + run: node scripts/semgrep-sync-issues.mjs semgrep.json diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 00000000..9a155424 --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,19 @@ +Archive/** +**/Archive/** + +EstateManagementUI.BlazorServer.Tests/** +EstateManagementUI.IntegrationTests/** + +bin/** +obj/** +.vs/** +.vscode/** +.idea/** +node_modules/** +TestResults/** + +**/*.g.cs +**/*.g.i.cs +**/*.designer.cs +**/*.Generated.cs +**/Generated/** diff --git a/scripts/semgrep-sync-issues.mjs b/scripts/semgrep-sync-issues.mjs new file mode 100644 index 00000000..86eac9d6 --- /dev/null +++ b/scripts/semgrep-sync-issues.mjs @@ -0,0 +1,256 @@ +import crypto from 'node:crypto'; +import fs from 'node:fs/promises'; + +const repo = process.env.GITHUB_REPOSITORY; +const token = process.env.GITHUB_TOKEN; +const threshold = normalizeSeverity(process.env.SEMGREP_MIN_SEVERITY || 'warning'); +const runUrl = process.env.GITHUB_RUN_ID && repo + ? `https://github.com/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}` + : ''; + +if (!repo) { + throw new Error('GITHUB_REPOSITORY is required.'); +} + +if (!token) { + throw new Error('GITHUB_TOKEN is required.'); +} + +const inputPath = process.argv[2] || 'semgrep.json'; +const raw = await fs.readFile(inputPath, 'utf8').catch(() => ''); +const parsed = raw.trim() ? JSON.parse(raw) : {}; +const results = Array.isArray(parsed.results) ? parsed.results : []; +const findings = results + .map(normalizeFinding) + .filter((finding) => finding && severityRank(finding.severity) >= threshold.rank); + +const seenMarkers = new Set(); +const existingIssues = await searchIssues(`repo:${repo} is:issue "semgrep-finding-id:" in:body`); + +for (const finding of findings) { + const marker = finding.marker; + seenMarkers.add(marker); + + const matches = await searchIssues(`repo:${repo} is:issue "${marker}" in:body`); + const canonical = chooseCanonicalIssue(matches); + + if (matches.length > 1) { + for (const duplicate of matches) { + if (canonical && duplicate.id === canonical.id) { + continue; + } + await closeIssue(duplicate.number, `Consolidated into #${canonical?.number ?? 'N/A'} for the same Semgrep finding marker.`); + } + } + + if (canonical) { + await upsertIssue(canonical, finding, marker); + } else { + await createIssue(finding, marker); + } +} + +for (const issue of existingIssues) { + const marker = extractMarker(issue.body || ''); + if (!marker || seenMarkers.has(marker)) { + continue; + } + + if (issue.state === 'open') { + await closeIssue(issue.number, 'Resolved by a later Semgrep scan.'); + } +} + +function normalizeFinding(result) { + const path = result?.path || result?.location?.path || ''; + const ruleId = result?.check_id || result?.rule_id || result?.ruleId || result?.id || 'unknown-rule'; + const message = normalizeText(result?.extra?.message || result?.message || ''); + const severity = normalizeSeverity(result?.extra?.severity || result?.severity || 'warning').name; + const startLine = result?.start?.line || result?.location?.start_line || result?.start_line || null; + const endLine = result?.end?.line || result?.location?.end_line || result?.end_line || startLine; + const snippet = normalizeText(result?.extra?.lines || result?.lines || ''); + + if (!path) { + return null; + } + + const markerSource = [ + ruleId, + cleanPath(path), + message, + snippet, + ].join('|'); + + return { + path: cleanPath(path), + ruleId, + message, + severity, + startLine, + endLine, + snippet, + marker: sha256(markerSource), + }; +} + +function chooseCanonicalIssue(issues) { + if (!issues.length) { + return null; + } + + const openIssues = issues.filter((issue) => issue.state === 'open'); + const pool = openIssues.length ? openIssues : issues; + + return pool + .slice() + .sort((left, right) => new Date(right.updated_at) - new Date(left.updated_at))[0]; +} + +async function upsertIssue(issue, finding, marker) { + const title = buildTitle(finding); + const body = buildBody(finding, marker); + + if (issue.state !== 'open') { + await api(`/repos/${repo}/issues/${issue.number}`, 'PATCH', { state: 'open' }); + } + + if (issue.title !== title || issue.body !== body) { + await api(`/repos/${repo}/issues/${issue.number}`, 'PATCH', { title, body }); + } +} + +async function createIssue(finding, marker) { + const title = buildTitle(finding); + const body = buildBody(finding, marker); + + await api(`/repos/${repo}/issues`, 'POST', { title, body }); +} + +async function closeIssue(number, comment) { + if (comment) { + await api(`/repos/${repo}/issues/${number}/comments`, 'POST', { body: comment }); + } + + await api(`/repos/${repo}/issues/${number}`, 'PATCH', { state: 'closed' }); +} + +function buildTitle(finding) { + const linePart = finding.startLine ? `:${finding.startLine}` : ''; + const title = `[Semgrep][${finding.severity.toUpperCase()}] ${finding.ruleId} ${finding.path}${linePart}`; + return title.length > 240 ? title.slice(0, 240) : title; +} + +function buildBody(finding, marker) { + const lines = finding.startLine && finding.endLine && finding.startLine !== finding.endLine + ? `${finding.startLine}-${finding.endLine}` + : (finding.startLine ? `${finding.startLine}` : 'unknown'); + + const parts = [ + ``, + ``, + ``, + ``, + '', + `Semgrep reported a ${finding.severity} finding in \`${finding.path}\`.`, + '', + `- Rule: \`${finding.ruleId}\``, + `- Path: \`${finding.path}\``, + `- Lines: \`${lines}\``, + `- Severity: \`${finding.severity}\``, + ]; + + if (runUrl) { + parts.push(`- Scan: ${runUrl}`); + } + + if (finding.message) { + parts.push('', '### Message', finding.message); + } + + if (finding.snippet) { + parts.push('', '### Match', '```text', finding.snippet, '```'); + } + + return parts.join('\n'); +} + +function extractMarker(body) { + const match = body.match(//i); + return match ? match[1] : null; +} + +function normalizeSeverity(value) { + const normalized = String(value || 'warning').trim().toLowerCase(); + + if (normalized === 'info' || normalized === 'information') { + return { name: 'info', rank: 1 }; + } + + if (normalized === 'warning' || normalized === 'warn') { + return { name: 'warning', rank: 2 }; + } + + if (normalized === 'error' || normalized === 'high' || normalized === 'critical') { + return { name: 'error', rank: 3 }; + } + + return { name: 'warning', rank: 2 }; +} + +function severityRank(value) { + return normalizeSeverity(value).rank; +} + +function normalizeText(value) { + return String(value || '').replace(/\s+/g, ' ').trim(); +} + +function cleanPath(value) { + return String(value || '').replace(/\\/g, '/'); +} + +function sha256(value) { + return crypto.createHash('sha256').update(value).digest('hex'); +} + +async function searchIssues(query) { + const items = []; + for (let page = 1; page < 10; page += 1) { + const result = await api(`/search/issues?q=${encodeURIComponent(query)}&per_page=100&page=${page}`); + if (!Array.isArray(result.items) || result.items.length === 0) { + break; + } + + items.push(...result.items.filter((item) => !item.pull_request)); + + if (result.items.length < 100) { + break; + } + } + + return items; +} + +async function api(path, method = 'GET', body) { + const response = await fetch(`https://api.github.com${path}`, { + method, + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + ...(body ? { 'Content-Type': 'application/json' } : {}), + }, + body: body ? JSON.stringify(body) : undefined, + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`GitHub API request failed for ${method} ${path}: ${response.status} ${response.statusText}\n${text}`); + } + + if (response.status === 204) { + return null; + } + + return response.json(); +}