Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
7c4baf8
feat(workflows): add weekly-security-scanning workflow
rezatnoMsirhC Apr 30, 2026
1206f8f
refactor(workflows): extract gh-security-scanning reusable workflow
rezatnoMsirhC Apr 30, 2026
5c764c2
feat(workflows): extract security scanning issue creation into reusab…
rezatnoMsirhC Apr 30, 2026
9882e45
refactor(workflows): replace owner/repo inputs with github context vars
rezatnoMsirhC Apr 30, 2026
96f79d8
chore(scripts): remove unused symlink for Get-CodeScanningAlerts.ps1
rezatnoMsirhC May 1, 2026
2dc5520
feat(workflows): rename gh-security-scanning to gh-code-scanning and …
rezatnoMsirhC May 1, 2026
24c5303
test(workflows): add temporary pull_request trigger for live validation
rezatnoMsirhC May 1, 2026
8e1cce4
fix(workflows): improve code scanning issue quality
rezatnoMsirhC May 1, 2026
01413f1
docs(skills): update gh-code-scanning SKILL.md output shape
rezatnoMsirhC May 1, 2026
bc030f7
Removing ms.date from gh-code-scanning skill
rezatnoMsirhC May 1, 2026
30d556c
fix(workflows): fix gh issue edit missing title, labels, and duplicat…
rezatnoMsirhC May 1, 2026
9d1e51e
fix(workflows): omit severity bracket when unknown, add Severity fall…
rezatnoMsirhC May 1, 2026
cf56d7e
test(skills): update gh-code-scanning tests for AffectedPaths rename
rezatnoMsirhC May 1, 2026
902d65f
revert(workflows): remove temporary pull_request trigger from weekly …
rezatnoMsirhC May 1, 2026
ab37e93
Merge branch 'main' into feat/1329-gh-security-weekly-scanning-workflow
rezatnoMsirhC May 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 32 additions & 20 deletions .github/skills/github/gh-code-scanning/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ name: gh-code-scanning
description: 'Retrieves and groups GitHub code scanning alerts by rule and severity using the gh CLI - Brought to you by microsoft/hve-core'
license: MIT
compatibility: 'Requires pwsh 7+ and gh CLI authenticated with the security_events scope. Bash script requires jq.'
ms.date: 2026-04-21
metadata:
authors: "microsoft/hve-core"
spec_version: "1.0"
Expand Down Expand Up @@ -41,12 +40,12 @@ This returns a JSON array of alert groups sorted by occurrence count, descending

## Parameters Reference

| Parameter | Type | Required | Default | Description |
|-----------------|--------|----------|---------|---------------------------------------------------------------------------|
| `-Owner` | String | Yes | | GitHub organization or user that owns the repository |
| `-Repo` | String | Yes | | Repository name |
| `-OutputFormat` | String | No | Table | Output format: agents must always use `Json` for programmatic consumption |
| `-Branch` | String | No | `main` | Branch to scope alert results |
| Parameter | Type | Required | Default | Description |
|-----------------|--------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------|
| `-Owner` | String | Yes | | GitHub organization or user that owns the repository |
| `-Repo` | String | Yes | | Repository name |
| `-OutputFormat` | String | No | Table | Output format: agents must always use `Json` for programmatic consumption; `GroupedJson` is accepted as an alias for `Json` |
| `-Branch` | String | No | `main` | Branch to scope alert results |

> These parameters apply to `Get-CodeScanningAlerts.ps1`. For bash script flags including `-s {severity}`, see the Script Reference section below.

Expand Down Expand Up @@ -108,36 +107,46 @@ Use `-Branch {branch}` to scope to a branch other than `main`.
"RuleId": "py/empty-except",
"Tool": "CodeQL",
"SecuritySeverity": null,
"Severity": "warning",
"Count": 23,
"SamplePaths": [
"AffectedPaths": [
"scripts/collections/Get-CollectionItems.py",
"scripts/linting/Validate-MarkdownFrontmatter.py"
]
],
"HasFilePaths": true,
"AlertUrl": "https://github.com/microsoft/hve-core/security/code-scanning/42",
"FindingDescription": "'except' clause does nothing but pass and there is no explanatory comment."
},
{
"RuleDescription": "Code injection",
"RuleId": "actions/code-injection/medium",
"Tool": "CodeQL",
"SecuritySeverity": "medium",
"Severity": "error",
"Count": 2,
"SamplePaths": [
"AffectedPaths": [
".github/workflows/validate.yml"
]
],
"HasFilePaths": true,
"AlertUrl": "https://github.com/microsoft/hve-core/security/code-scanning/17",
"FindingDescription": "Potential code injection in ${{ inputs.version }}, which may be controlled by an external user."
},
{
"RuleDescription": "Branch-Protection",
"RuleId": "BranchProtectionID",
"Tool": "Scorecard",
"SecuritySeverity": "high",
"Severity": "error",
"Count": 1,
"SamplePaths": [
"no file associated with this alert"
]
"AffectedPaths": [],
"HasFilePaths": false,
"AlertUrl": "https://github.com/microsoft/hve-core/security/code-scanning/1",
"FindingDescription": "score is 9: branch protection is not maximal on development and all release branches"
}
]
```

`SecuritySeverity` is `null` when the rule has no severity tier assigned. `SamplePaths` is always a JSON array. When an alert has no associated source file (for example, `BranchProtectionID`), the array contains the sentinel string `"no file associated with this alert"`.
`SecuritySeverity` is `null` for code quality rules that have no security classification; `Severity` (the non-security rule severity: `error`, `warning`, `note`, `none`) provides a fallback. `AffectedPaths` is always a JSON array of unique, sorted file paths with sentinel strings filtered out. `HasFilePaths` is `false` and `AffectedPaths` is `[]` when an alert has no associated source file (for example, `BranchProtectionID`). `AlertUrl` links directly to the alert in the GitHub Security tab. `FindingDescription` is the most recent alert message text.

### Get single alert detail

Expand All @@ -149,11 +158,14 @@ gh api repos/{owner}/{repo}/code-scanning/alerts/{alert_number}

### List affected file paths

Use `-OutputFormat Json` and read the `SamplePaths` field from each rule group. The JSON output includes `RuleDescription`, `RuleId`, `Tool`, `SecuritySeverity`, `Count`, and `SamplePaths` (unique, sorted file paths) per group.
Use `-OutputFormat Json` and read the `AffectedPaths` field from each rule group. The JSON output includes `RuleDescription`, `RuleId`, `Tool`, `SecuritySeverity`, `Severity`, `Count`, `AffectedPaths` (unique, sorted file paths), `HasFilePaths` (boolean: `false` for repo-level rules that have no associated source file), `AlertUrl` (string: direct link to the alert in the GitHub Security tab), and `FindingDescription` (string: most recent alert message text from the analysis tool) per group.

### Key fields

* `rule.security_severity_level`: severity tier: `critical`, `high`, `medium`, or `low`
These are GitHub API response field paths, not output object properties. The grouped output object field names are listed in the JSON output shape section above.

* `rule.security_severity_level`: security severity tier: `critical`, `high`, `medium`, or `low`; `null` for code quality rules
* `rule.severity`: non-security rule severity: `error`, `warning`, `note`, or `none`; always populated
* `rule.id`: rule identifier used for deduplication and cross-referencing
* `tool.name`: analysis tool that produced the alert (for example, `CodeQL`)
* `most_recent_instance.location.path`: source file path of the most recent alert occurrence
Expand Down Expand Up @@ -199,12 +211,12 @@ if [[ -z "$existing" ]]; then
## Code Scanning Alert: {rule_description}

**Rule:** \`{rule_id}\`
**Severity:** {security_severity}
$([ -n "{severity}" ] && echo "**Severity:** {severity}")
**Tool:** {tool}
**Affected files:** {count} occurrences

### Sample affected paths
{sample_paths}
### Affected paths
{affected_paths}
"
fi
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,23 @@ if ($MyInvocation.InvocationName -ne '.') {
$Grouped = $Alerts |
Group-Object { $_.rule.description } |
ForEach-Object {
$paths = @(
$_.Group |
ForEach-Object { $_.most_recent_instance.location.path } |
Where-Object { $_ -and $_ -notmatch '(?i)no file' } |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Overly broad sentinel filter

The regex -notmatch '(?i)no file' will silently drop any path containing the substring no file (case-insensitive), including legitimate paths such as /services/no-file-cache/handler.ps1 or scripts/nofile-cleanup.py.

Suggested fix: Match the exact sentinel string the GitHub API returns for repo-level alerts:

Where-Object { $_ -and $_ -ne 'no file associated with this alert' }

This is unambiguous, doesn't rely on a substring heuristic, and matches the existing test fixture string verbatim.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 M3. Path filter regex is too loose

.github/skills/github/gh-code-scanning/scripts/Get-CodeScanningAlerts.ps1

Where-Object { $_ -and $_ -notmatch '(?i)no file' }

(?i)no file is an unanchored substring, so any future legitimate path containing the bytes "no file" (case-insensitive - e.g. docs/known-files-list.md? no; path/to/no-file-found.md? yes) gets dropped. Anchor the match to the GitHub sentinel exactly:

Where-Object { $_ -and $_ -ne 'no file associated with this alert' }

Sort-Object -Unique
)
[PSCustomObject]@{
RuleDescription = $_.Name
RuleId = $_.Group[0].rule.id
Tool = $_.Group[0].tool.name
SecuritySeverity = $_.Group[0].rule.security_severity_level
Count = $_.Count
SamplePaths = @($_.Group | ForEach-Object { $_.most_recent_instance.location.path } | Sort-Object -Unique)
RuleDescription = $_.Name
RuleId = $_.Group[0].rule.id
Tool = $_.Group[0].tool.name
SecuritySeverity = $_.Group[0].rule.security_severity_level
Severity = $_.Group[0].rule.severity
Count = $_.Count
AffectedPaths = $paths
HasFilePaths = ($paths.Count -gt 0)
AlertUrl = $_.Group[0].html_url
FindingDescription = $_.Group[0].most_recent_instance.message.text
}
} |
Sort-Object -Property Count -Descending
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,31 +84,31 @@ Describe 'Get-CodeScanningAlerts' -Tag 'Unit' {
$parsed.Count | Should -BeGreaterThan 0
}

It 'Serializes SamplePaths as a JSON array even when only one path exists' {
It 'Serializes AffectedPaths as a JSON array even when only one path exists' {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 M6. New skill output fields are not under unit test

.github/skills/github/gh-code-scanning/tests/Get-CodeScanningAlerts.Tests.ps1

The PR adds Severity, AlertUrl, and FindingDescription to the grouped output. Tests cover only AffectedPaths/HasFilePaths. Add at least one assertion per new field (e.g. $parsed[0].Severity | Should -Be 'warning', $parsed[0].AlertUrl | Should -Match '/security/code-scanning/') so a future regression that drops one of these silently does not also silently degrade the issue body.

# js/xss has a single occurrence; verify the raw JSON uses bracket notation,
# not a bare string (ConvertFrom-Json re-unwraps single-element arrays so
# the raw string is the authoritative check)
$result = & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' -OutputFormat Json
$rawJson = $result | Out-String

$rawJson | Should -Match '"SamplePaths":\s*\['
$rawJson | Should -Match '"AffectedPaths":\s*\['
}

It 'Serializes SamplePaths as a JSON array when alert has no associated file path' {
It 'Serializes AffectedPaths as empty array and sets HasFilePaths false when alert has no associated file path' {
$noPathJson = '[{"number":10,"rule":{"id":"BranchProtectionID","description":"Branch-Protection","security_severity_level":"high"},"tool":{"name":"Scorecard"},"most_recent_instance":{"location":{"path":"no file associated with this alert"}}}]'
${Function:gh} = {
$global:LASTEXITCODE = 0
return $noPathJson
}.GetNewClosure()

$result = & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' -OutputFormat Json
$rawJson = $result | Out-String
$parsed = $result | ConvertFrom-Json

$rawJson | Should -Match '"SamplePaths":\s*\['
$rawJson | Should -Match 'no file associated with this alert'
$parsed[0].AffectedPaths | Should -HaveCount 0
$parsed[0].HasFilePaths | Should -BeFalse
}

It 'Deduplicates and sorts SamplePaths across multiple occurrences of the same rule' {
It 'Deduplicates and sorts AffectedPaths across multiple occurrences of the same rule' {
$multiPathJson = '[{"number":1,"rule":{"id":"py/empty-except","description":"Empty except","security_severity_level":null},"tool":{"name":"CodeQL"},"most_recent_instance":{"location":{"path":"scripts/b.py"}}},{"number":2,"rule":{"id":"py/empty-except","description":"Empty except","security_severity_level":null},"tool":{"name":"CodeQL"},"most_recent_instance":{"location":{"path":"scripts/a.py"}}},{"number":3,"rule":{"id":"py/empty-except","description":"Empty except","security_severity_level":null},"tool":{"name":"CodeQL"},"most_recent_instance":{"location":{"path":"scripts/a.py"}}}]'
${Function:gh} = {
$global:LASTEXITCODE = 0
Expand All @@ -118,9 +118,9 @@ Describe 'Get-CodeScanningAlerts' -Tag 'Unit' {
$result = & $script:ScriptPath -Owner 'testorg' -Repo 'testrepo' -OutputFormat Json
$parsed = $result | ConvertFrom-Json

$parsed[0].SamplePaths | Should -HaveCount 2
$parsed[0].SamplePaths[0] | Should -Be 'scripts/a.py'
$parsed[0].SamplePaths[1] | Should -Be 'scripts/b.py'
$parsed[0].AffectedPaths | Should -HaveCount 2
$parsed[0].AffectedPaths[0] | Should -Be 'scripts/a.py'
$parsed[0].AffectedPaths[1] | Should -Be 'scripts/b.py'
}
}

Expand Down
113 changes: 113 additions & 0 deletions .github/workflows/create-gh-code-scanning-issues.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
name: Create GitHub Code Scanning Issues

on:
workflow_call:
inputs:
artifact-name:
description: 'Name of the artifact containing code scanning alerts'
required: false
type: string
default: gh-code-scanning-alerts

permissions:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 M5. Reusable workflow does not declare actions: read for download-artifact@v8

.github/workflows/create-gh-code-scanning-issues.yml

Same-run artifact downloads are fine, but actions/download-artifact@v8 documents actions: read as required when artifacts could come from another run. This workflow only consumes its own job's artifact today, so no immediate breakage; still, declaring actions: read future-proofs against run-id parameterization, which is a natural extension of a reusable workflow.

issues: write
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 M4. Unused security-events: read on issue-creation job

.github/workflows/create-gh-code-scanning-issues.yml

The issue-creation job only reads a workflow artifact and writes issues. security-events: read is granted at both workflow and job scope but the job never calls a security-events API. Drop it (principle of least privilege).

security-events: read
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 security-events: read is not used by this job

This workflow downloads a pre-generated artifact and calls gh issue create/edit. It never queries the code-scanning API directly, so security-events: read is unnecessary here. Removing it keeps the permissions footprint at the minimum required (principle of least privilege per the workflow convention instructions).

permissions:
  issues: write
  # Remove: security-events: read

Apply the same change to the job-level permissions block on line 22.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 security-events: read is not required by this workflow

This workflow only (1) downloads a pre-uploaded artifact and (2) creates/updates GitHub issues via gh issue CLI commands. Neither operation requires direct access to the GitHub Security Events API.

Per the repository's workflow convention, permissions follow the principle of least privilege. security-events: read should be removed from both the top-level permissions block (here) and the job-level permissions block (line 22).

Suggested fix:

permissions:
  issues: write


jobs:
create-gh-code-scanning-issues:
name: Create GitHub Code Scanning Issues
runs-on: ubuntu-latest
permissions:
issues: write
security-events: read
env:
GH_TOKEN: ${{ github.token }}
OWNER: ${{ github.repository_owner }}
REPO: ${{ github.event.repository.name }}
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_RUN_ID: ${{ github.run_id }}
steps:
- name: Download alerts artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ inputs.artifact-name }}

- name: Create backlog issues for new findings
shell: bash
run: |
while IFS= read -r alert; do
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing strict error handling — set -euo pipefail required

Per the bash script conventions for this repository (.github/instructions/hve-core/bash.instructions.md), every bash run: block must begin with set -euo pipefail to ensure the step fails immediately on any unhandled error. Without it, a failed jq call or a failed gh issue create silently swallows the error and the loop continues, potentially producing incomplete or malformed issue tracking.

      - name: Create backlog issues for new findings
        shell: bash
        run: |
          set -euo pipefail
          while IFS= read -r alert; do

RULE_ID=$(echo "$alert" | jq -r '.RuleId')
RULE_DESC=$(echo "$alert" | jq -r '.RuleDescription')
SEVERITY=$(echo "$alert" | jq -r '.SecuritySeverity // .Severity // empty')
TOOL=$(echo "$alert" | jq -r '.Tool')
COUNT=$(echo "$alert" | jq -r '.Count')
HAS_FILE_PATHS=$(echo "$alert" | jq -r '.HasFilePaths')
ALERT_URL=$(echo "$alert" | jq -r '.AlertUrl // ""')
FINDING_DESC=$(echo "$alert" | jq -r '.FindingDescription // ""')
MARKER="automation:security-scan:${RULE_ID}"
if [[ -n "$SEVERITY" ]]; then
ISSUE_TITLE="[Security][${SEVERITY}] ${RULE_DESC}"
else
ISSUE_TITLE="[Security] ${RULE_DESC}"
fi
OCCURRENCE_WORD="$([ "$COUNT" = "1" ] && echo "occurrence" || echo "occurrences")"
DETECTION_DATE=$(date -u '+%Y-%m-%d')
RUN_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
REPO_BASE="https://github.com/${OWNER}/${REPO}/blob/main"

if [[ "$HAS_FILE_PATHS" == "true" ]]; then
AFFECTED_LIST=$(echo "$alert" | jq -r --arg base "$REPO_BASE" '.AffectedPaths[] | "- [\(.)](\($base + "/" + .))"')
PATHS_SECTION=$'### Affected paths\n\n'"${AFFECTED_LIST}"
else
PATHS_SECTION=$'### Repository configuration finding\n\nThis alert refers to a repository-level configuration setting with no associated source file.'
fi

existing=$(gh issue list \
--repo "${OWNER}/${REPO}" \
--search "\"${MARKER}\" in:body" \
--state open --json number --jq '.[0].number // empty')

ISSUE_BODY="<!-- ${MARKER} -->
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 M1. Bash here-doc style is fragile and non-idiomatic

.github/workflows/create-gh-code-scanning-issues.yml

The body is built with a multi-line double-quoted assignment whose internal indentation (10 spaces) only happens to render correctly because YAML strips the block-scalar indent first. Any future edit that nudges indentation by one column will silently turn the issue body into a code block. Use a cat <<'EOF' heredoc piped through envsubst, or build the body with printf using explicit \n, so the content is independent of YAML indent.

Suggested rewrite (heredoc):

ISSUE_BODY=$(cat <<EOF
<!-- ${MARKER} -->
## Code Scanning Alert: ${RULE_DESC}

**Rule:** \`${RULE_ID}\`
$([ -n "${SEVERITY}" ] && echo "**Severity:** ${SEVERITY}")
**Tool:** ${TOOL}
...
EOF
)

This also removes the trailing-quote-on-its-own-line pattern that is easy to misalign.

## Code Scanning Alert: ${RULE_DESC}

**Rule:** \`${RULE_ID}\`
$([ -n "${SEVERITY}" ] && echo "**Severity:** ${SEVERITY}")
**Tool:** ${TOOL}
**Occurrences:** ${COUNT} ${OCCURRENCE_WORD}
$([ -n "${ALERT_URL}" ] && echo "**Alert:** ${ALERT_URL}")

### What was found
$([ -n "${FINDING_DESC}" ] && echo "${FINDING_DESC}" || echo "See the linked alert for details.")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Shell expansion inside a double-quoted string — potential injection vector

ISSUE_BODY is assembled as a double-quoted string ("..."). Subshell expressions such as $([ -n "\$\{FINDING_DESC}" ] && echo "\$\{FINDING_DESC}") are expanded by bash inside double quotes. If FINDING_DESC (derived from most_recent_instance.message.text in a GitHub API response) contains a pattern like $(malicious_command), bash will execute it during variable assignment.

While the risk is low in practice because GitHub API values are controlled, defense-in-depth recommends preventing expansion by writing the body to a temporary file instead of a shell variable, or by using printf '%s' with a heredoc:

ISSUE_BODY=$(cat <<'BODY_EOF'
<!-- \$\{MARKER} -->
BODY_EOF
printf "**Rule:** \`%s\`\n**Tool:** %s\n...\n" "\$\{RULE_ID}" "\$\{TOOL}" >> /tmp/issue_body_$$.md)

Alternatively, build the body with jq --rawfile or jq -n --arg ... to keep all values properly escaped and isolated from shell expansion.


${PATHS_SECTION}

---
**Detection Date:** ${DETECTION_DATE}
**Workflow Run:** ${RUN_URL}

### Action Required
- [ ] Review the alert and confirm it is not a false positive
- [ ] Remediate or dismiss the finding with a documented reason
- [ ] Close this issue after the fix is merged
"

if [[ -z "$existing" ]]; then
gh issue create \
--repo "${OWNER}/${REPO}" \
--title "${ISSUE_TITLE}" \
--label "security,automated,needs-triage" \
--body "${ISSUE_BODY}"
else
gh issue edit "${existing}" \
--repo "${OWNER}/${REPO}" \
--title "${ISSUE_TITLE}" \
--body "${ISSUE_BODY}"
gh issue edit "${existing}" \
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 M2. automated label is referenced but missing from labels.yml

.github/workflows/create-gh-code-scanning-issues.yml and .github/workflows/create-gh-code-scanning-issues.yml

Workflows here, in create-stale-docs-issues.yml, and in weekly-security-maintenance.yml all attach automated, but .github/labels.yml only declares automation (line 70). The automated label currently exists in the live repo only because someone created it ad-hoc; a fresh fork or a future sweep will fail issue creation with could not add label. Either:

  • Add automated to labels.yml (preferred - preserves existing convention), or
  • Switch all three workflows to automation.

This is pre-existing technical debt, but it is being entrenched by this PR; fix it here.

--repo "${OWNER}/${REPO}" \
--add-label "automated,needs-triage"
gh issue comment "${existing}" \
--repo "${OWNER}/${REPO}" \
--body "Weekly scan update: ${COUNT} ${OCCURRENCE_WORD} as of ${DETECTION_DATE}."
fi
done < <(jq -c '.[]' alerts.json)
67 changes: 67 additions & 0 deletions .github/workflows/create-gh-security-scanning-issues.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Create GitHub Security Scanning Issues
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 B1. Orphan/dead workflow file: create-gh-security-scanning-issues.yml

.github/workflows/create-gh-security-scanning-issues.yml

This 67-line file is a leftover from the rename in commit 2dc55203 feat(workflows): rename gh-security-scanning to gh-code-scanning. It is never referenced anywhere (weekly-gh-code-scanning.yml only calls create-gh-code-scanning-issues.yml), still uses the old SamplePaths field name (which the skill no longer emits), and contradicts the PR description which says "three new workflows added".

If left in place it is unreachable code that future readers will mistake for a second active code path, and any reusable workflow consumer who picks the wrong file will get broken behavior (it expects the old artifact name gh-security-scanning-alerts and the removed SamplePaths field).

Action: Delete .github/workflows/create-gh-security-scanning-issues.yml.


on:
workflow_call:
inputs:
artifact-name:
description: 'Name of the artifact containing code scanning alerts'
required: false
type: string
default: gh-security-scanning-alerts

permissions:
issues: write
security-events: read

jobs:
create-gh-security-scanning-issues:
name: Create GitHub Security Scanning Issues
runs-on: ubuntu-latest
permissions:
issues: write
security-events: read
env:
GH_TOKEN: ${{ github.token }}
OWNER: ${{ github.repository_owner }}
REPO: ${{ github.event.repository.name }}
steps:
- name: Download alerts artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ inputs.artifact-name }}

- name: Create backlog issues for new findings
shell: bash
run: |
while IFS= read -r alert; do
RULE_ID=$(echo "$alert" | jq -r '.RuleId')
RULE_DESC=$(echo "$alert" | jq -r '.RuleDescription')
SEVERITY=$(echo "$alert" | jq -r '.SecuritySeverity // "unspecified"')
TOOL=$(echo "$alert" | jq -r '.Tool')
COUNT=$(echo "$alert" | jq -r '.Count')
PATHS=$(echo "$alert" | jq -r '.SamplePaths | join(", ")')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Stale field reference — workflow will fail at runtime

This line references .SamplePaths, a field renamed to .AffectedPaths in Get-CodeScanningAlerts.ps1 as part of this same PR. jq silently returns null for missing keys, so PATHS will be the literal string null in every issue body.

Beyond the field name, this file has several additional problems that suggest it is an accidental inclusion of an early draft:

  • Artifact name mismatch: expects gh-security-scanning-alerts (line 10), but gh-code-scanning.yml only uploads gh-code-scanning-alerts. The download step would fail with "no artifact found".
  • Not mentioned in the PR description: the description documents exactly three workflows (gh-code-scanning.yml, create-gh-code-scanning-issues.yml, weekly-gh-code-scanning.yml). This file is a fourth that is unaccounted for.
  • Not called by any orchestrator: weekly-gh-code-scanning.yml only calls create-gh-code-scanning-issues.yml. This workflow is dead code.
  • Stale deduplication logic: searches by title ("[Security] \$\{RULE_DESC}" in:title) rather than the body marker (\$\{MARKER} in:body) used in create-gh-code-scanning-issues.yml, and it never updates existing issues.

Recommended fix: Remove .github/workflows/create-gh-security-scanning-issues.yml from the PR entirely. If a distinct "security scanning" variant is needed in the future, it should be addressed in a separate, fully described PR.

MARKER="automation:security-scan:${RULE_ID}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Stale field name — SamplePaths was renamed to AffectedPaths in this PR

This line reads .SamplePaths from the JSON output, but Get-CodeScanningAlerts.ps1 was updated in this same PR to emit AffectedPaths instead. As written this line produces an empty string for every alert.

# Fix: use the new field name
PATHS=$(echo "$alert" | jq -r '.AffectedPaths | join(", ")')

More broadly, this entire file appears to be a leftover from an earlier development iteration. It is not referenced by any orchestrator workflow in this PR, and its deduplication strategy (title search on line 55) diverges from the body-marker approach adopted by create-gh-code-scanning-issues.yml. Please either remove this file or update it to use the current API and wire it into the scheduler.

existing=$(gh issue list \
--repo "${OWNER}/${REPO}" \
--search "\"[Security] ${RULE_DESC}\" in:title" \
--state open --json number --jq '.[0].number // empty')

if [[ -z "$existing" ]]; then
gh issue create \
--repo "${OWNER}/${REPO}" \
--title "[Security] ${RULE_DESC}" \
--label "security" \
--body "<!-- ${MARKER} -->
## Code Scanning Alert: ${RULE_DESC}

**Rule:** \`${RULE_ID}\`
**Severity:** ${SEVERITY}
**Tool:** ${TOOL}
**Affected files:** ${COUNT} occurrences

### Sample affected paths
${PATHS}
"
fi
done < <(jq -c '.[]' alerts.json)
Loading
Loading