diff --git a/.github/workflows/aquasec-scan.yml b/.github/workflows/aquasec-scan.yml index 9264a1a..27e3e01 100644 --- a/.github/workflows/aquasec-scan.yml +++ b/.github/workflows/aquasec-scan.yml @@ -105,11 +105,17 @@ jobs: needs: aquasec-scan runs-on: ubuntu-latest steps: + - name: Checkout caller repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + with: + persist-credentials: false + path: caller-repo + - name: Checkout security scripts uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 with: repository: AbsaOSS/organizational-workflows - ref: master + ref: feature/8-ability-to-configure-labels-used path: org-workflows persist-credentials: false @@ -131,4 +137,11 @@ jobs: PROJECT_NUMBER: ${{ inputs.project-number }} PROJECT_ORG: ${{ inputs.project-org }} run: | - org-workflows/github/security/sync_security_alerts.sh + SYNC_ARGS=() + # If the caller repo ships a .github/labels.yml, use it to override + # the default label names. + CALLER_LABELS="caller-repo/.github/labels.yml" + if [[ -f "$CALLER_LABELS" ]]; then + SYNC_ARGS+=(--label-config "$CALLER_LABELS") + fi + org-workflows/github/security/sync_security_alerts.sh "${SYNC_ARGS[@]}" diff --git a/github/security/README.md b/github/security/README.md index 2196124..217cd71 100644 --- a/github/security/README.md +++ b/github/security/README.md @@ -17,6 +17,7 @@ In one sentence: SARIF uploads create alerts; these scripts sync alerts into Iss - [How to adopt a shared workflow](#how-to-adopt-a-shared-workflow) - [Aquasec Night Scan](#aquasec-night-scan) - [Remove sec:adept-to-close on close](#remove-secadept-to-close-on-close) +- [Label configuration](#label-configuration) - [Labels (contract)](#labels-contract) - [Issue metadata (secmeta)](#issue-metadata-secmeta) - [Issue structure](#issue-structure) @@ -41,6 +42,7 @@ In one sentence: SARIF uploads create alerts; these scripts sync alerts into Iss | `check_labels.sh` | Verify that all labels required by the automation exist in the repository | `gh` | | `collect_alert.sh` | Fetch and normalize code scanning alerts into `alerts.json` | `gh`, `jq` | | `promote_alerts.py` | Create/update parent+child Issues from `alerts.json` and link children under parents | `gh` | +| `labels.yml` | Label configuration: maps logical label purposes to actual GitHub label names | — | | `send_to_teams.py` | Send a Markdown message to a Microsoft Teams channel via Incoming Webhook | `requests` | | `extract_team_security_stats.py` | Snapshot security Issues for a team across repos | `PyGithub`, `GITHUB_TOKEN` | | `derive_team_security_metrics.py` | Compute metrics/deltas from snapshots | stdlib | @@ -205,41 +207,170 @@ jobs: > **Note:** The calling repository must grant the permissions the reusable workflow needs (listed in each workflow file). For cross-organization calls the reusable workflow repository must be set to "Accessible from repositories in the organization" under **Settings → Actions → General**. +## Label configuration + +The four core labels used by the automation pipeline are **configurable** via a `labels.yml` file placed in the `github/security/` directory (next to the scripts). This lets each consuming repository map the logical label purposes to its own label names. + +### Default `labels.yml` + +```yaml +# Core +scope_security: "scope:security" +type_tech_debt: "type:tech-debt" +epic: "epic" + +# Lifecycle +adept_to_close: "sec:adept-to-close" + +# Source +src_aquasec_sarif: "sec:src/aquasec-sarif" + +# State +state_postponed: "sec:state/postponed" +state_needs_review: "sec:state/needs-review" + +# Severity +sev_critical: "sec:sev/critical" +sev_high: "sec:sev/high" +sev_medium: "sec:sev/medium" +sev_low: "sec:sev/low" + +# Closure reasons +close_fixed: "sec:close/fixed" +close_false_positive: "sec:close/false-positive" +close_accepted_risk: "sec:close/accepted-risk" +close_not_applicable: "sec:close/not-applicable" + +# Postpone reasons +postpone_vendor: "sec:postpone/vendor" +postpone_platform: "sec:postpone/platform" +postpone_roadmap: "sec:postpone/roadmap" +postpone_other: "sec:postpone/other" +``` + +### Customising labels + +To override any label, edit `labels.yml` in your copy of the scripts (or provide a path at runtime). For example, if your repository uses `type:epic` instead of `epic`: + +```yaml +epic: "type:epic" +``` + +Only the keys you include are overridden; missing keys fall back to the built-in defaults shown above. + +### Passing a custom config path + +All three entrypoints accept a `--label-config` flag: + +```bash +# Shell wrapper +./sync_security_alerts.sh --repo my-org/my-repo --label-config /path/to/labels.yml + +# Label check only +./check_labels.sh --repo my-org/my-repo --label-config /path/to/labels.yml + +# Python promote step +python3 promote_alerts.py --file alerts.json --label-config /path/to/labels.yml +``` + +When `--label-config` is omitted, the scripts look for `labels.yml` in their own directory. If that file is absent, built-in defaults are used (identical to the table above). + +### Configurable keys + +#### Core (applied by the pipeline) + +| Key | Default value | Purpose | +| --- | --- | --- | +| `scope_security` | `scope:security` | Applied to every security issue; used to mine existing issues | +| `type_tech_debt` | `type:tech-debt` | Applied to every security issue | +| `epic` | `epic` | Applied to parent (grouping) issues | +| `adept_to_close` | `sec:adept-to-close` | Applied to orphan child issues that no longer have a matching alert | + +#### Source + +| Key | Default value | Purpose | +| --- | --- | --- | +| `src_aquasec_sarif` | `sec:src/aquasec-sarif` | Identifies the scan source | + +#### State + +| Key | Default value | Purpose | +| --- | --- | --- | +| `state_postponed` | `sec:state/postponed` | Issue has been postponed | +| `state_needs_review` | `sec:state/needs-review` | Issue needs manual review | + +#### Severity + +| Key | Default value | Purpose | +| --- | --- | --- | +| `sev_critical` | `sec:sev/critical` | Critical severity | +| `sev_high` | `sec:sev/high` | High severity | +| `sev_medium` | `sec:sev/medium` | Medium severity | +| `sev_low` | `sec:sev/low` | Low severity | + +#### Closure reasons + +| Key | Default value | Purpose | +| --- | --- | --- | +| `close_fixed` | `sec:close/fixed` | Fixed in code | +| `close_false_positive` | `sec:close/false-positive` | Not a real finding | +| `close_accepted_risk` | `sec:close/accepted-risk` | Risk acknowledged | +| `close_not_applicable` | `sec:close/not-applicable` | Not applicable to this repo | + +#### Postpone reasons + +| Key | Default value | Purpose | +| --- | --- | --- | +| `postpone_vendor` | `sec:postpone/vendor` | Waiting on vendor fix | +| `postpone_platform` | `sec:postpone/platform` | Platform constraint | +| `postpone_roadmap` | `sec:postpone/roadmap` | Scheduled for a later milestone | +| `postpone_other` | `sec:postpone/other` | Other reason (detail in comment) | + ## Labels (contract) -This repository contains multiple scripts with different “label contracts”: +All label names used by this automation are configurable via `labels.yml` (see [Label configuration](#label-configuration) above). The values below are the **defaults**. - `promote_alerts.py` mines existing issues by `--issue-label` (default: `scope:security`) and ensures baseline labels `scope:security` and `type:tech-debt` on child/parent issues it creates/updates. ### Source -- `sec:src/aquasec-sarif` +| Config key | Default label | +| --- | --- | +| `src_aquasec_sarif` | `sec:src/aquasec-sarif` | ### State -- `sec:state/postponed` -- `sec:state/needs-review` +| Config key | Default label | +| --- | --- | +| `state_postponed` | `sec:state/postponed` | +| `state_needs_review` | `sec:state/needs-review` | ### Severity -- `sec:sev/critical` -- `sec:sev/high` -- `sec:sev/medium` -- `sec:sev/low` +| Config key | Default label | +| --- | --- | +| `sev_critical` | `sec:sev/critical` | +| `sev_high` | `sec:sev/high` | +| `sev_medium` | `sec:sev/medium` | +| `sev_low` | `sec:sev/low` | ### Closure reasons -- `sec:close/fixed` -- `sec:close/false-positive` -- `sec:close/accepted-risk` -- `sec:close/not-applicable` +| Config key | Default label | +| --- | --- | +| `close_fixed` | `sec:close/fixed` | +| `close_false_positive` | `sec:close/false-positive` | +| `close_accepted_risk` | `sec:close/accepted-risk` | +| `close_not_applicable` | `sec:close/not-applicable` | ### Postpone reasons -- `sec:postpone/vendor` -- `sec:postpone/platform` -- `sec:postpone/roadmap` -- `sec:postpone/other` +| Config key | Default label | +| --- | --- | +| `postpone_vendor` | `sec:postpone/vendor` | +| `postpone_platform` | `sec:postpone/platform` | +| `postpone_roadmap` | `sec:postpone/roadmap` | +| `postpone_other` | `sec:postpone/other` | ## Issue metadata (secmeta) diff --git a/github/security/check_labels.sh b/github/security/check_labels.sh index a1abb8c..b177a15 100755 --- a/github/security/check_labels.sh +++ b/github/security/check_labels.sh @@ -15,25 +15,33 @@ # limitations under the License. # # Check that required labels exist in a GitHub repo using gh + jq. +# Labels are loaded from labels.yml when available; otherwise built-in +# defaults are used. +# # Usage: # ./check_labels.sh --repo owner/repo +# ./check_labels.sh --repo owner/repo --label-config /path/to/labels.yml set -euo pipefail -REQUIRED_LABELS=( - "scope:security" - "type:tech-debt" - "epic" - "sec:adept-to-close" -) +# -- Built-in defaults (same as labels.yml ships with) -------------------- +_DEFAULT_SCOPE_SECURITY="scope:security" +_DEFAULT_TYPE_TECH_DEBT="type:tech-debt" +_DEFAULT_EPIC="epic" +_DEFAULT_ADEPT_TO_CLOSE="sec:adept-to-close" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LABEL_CONFIG="" repo="" while [[ $# -gt 0 ]]; do case "$1" in --repo) repo="${2:-}"; shift 2;; + --label-config) + LABEL_CONFIG="${2:-}"; shift 2;; -h|--help) - echo "Usage: $0 --repo owner/repo"; exit 0;; + echo "Usage: $0 --repo owner/repo [--label-config path/to/labels.yml]"; exit 0;; *) echo "Unknown argument: $1" >&2; exit 2;; esac @@ -44,6 +52,36 @@ if [[ -z "$repo" ]]; then exit 2 fi +if [[ -z "$LABEL_CONFIG" ]]; then + LABEL_CONFIG="$SCRIPT_DIR/labels.yml" +fi + +# Helper: read a value from the flat YAML config +# Usage: _cfg_val +_cfg_val() { + local key="$1" default="$2" + if [[ ! -f "$LABEL_CONFIG" ]]; then + printf '%s' "$default" + return + fi + + local val + val="$(grep -E "^${key}\s*:" "$LABEL_CONFIG" 2>/dev/null | head -1 \ + | sed -E 's/^[^:]+:\s*//; s/^["'"'"']//; s/["'"'"']\s*(#.*)?$//; s/\s*#.*$//' )" || true + if [[ -n "$val" ]]; then + printf '%s' "$val" + else + printf '%s' "$default" + fi +} + +REQUIRED_LABELS=( + "$(_cfg_val scope_security "$_DEFAULT_SCOPE_SECURITY")" + "$(_cfg_val type_tech_debt "$_DEFAULT_TYPE_TECH_DEBT")" + "$(_cfg_val epic "$_DEFAULT_EPIC")" + "$(_cfg_val adept_to_close "$_DEFAULT_ADEPT_TO_CLOSE")" +) + if ! json_out="$(gh label list --repo "$repo" --json name --limit 500 2>/dev/null)"; then echo "ERROR: failed to list labels for $repo" >&2 exit 1 diff --git a/github/security/labels.yml b/github/security/labels.yml new file mode 100644 index 0000000..a45738e --- /dev/null +++ b/github/security/labels.yml @@ -0,0 +1,44 @@ +# Label configuration for the security automation pipeline. +# +# Each key is a logical label purpose; the value is the actual GitHub label +# name used in your repository. Override any value to match your own +# labelling scheme. +# +# Example – if your repo uses "type:epic" instead of "epic": +# epic: "type:epic" +# +# The file is optional. When absent or when a key is missing, the built-in +# defaults (shown below) are used. + +# -- Core labels applied to every security issue +scope_security: "scope:security" +type_tech_debt: "type:tech-debt" +epic: "epic" + +# -- Lifecycle labels +adept_to_close: "sec:adept-to-close" + +# -- Source +src_aquasec_sarif: "sec:src/aquasec-sarif" + +# -- State +state_postponed: "sec:state/postponed" +state_needs_review: "sec:state/needs-review" + +# -- Severity +sev_critical: "sec:sev/critical" +sev_high: "sec:sev/high" +sev_medium: "sec:sev/medium" +sev_low: "sec:sev/low" + +# -- Closure reasons +close_fixed: "sec:close/fixed" +close_false_positive: "sec:close/false-positive" +close_accepted_risk: "sec:close/accepted-risk" +close_not_applicable: "sec:close/not-applicable" + +# -- Postpone reasons +postpone_vendor: "sec:postpone/vendor" +postpone_platform: "sec:postpone/platform" +postpone_roadmap: "sec:postpone/roadmap" +postpone_other: "sec:postpone/other" diff --git a/github/security/promote_alerts.py b/github/security/promote_alerts.py index 59710aa..02afcf8 100644 --- a/github/security/promote_alerts.py +++ b/github/security/promote_alerts.py @@ -57,7 +57,7 @@ from shared.priority import parse_severity_priority_map from utils.alert_parser import load_open_alerts_from_file -from utils.constants import LABEL_SCOPE_SECURITY +from utils.constants import LABEL_SCOPE_SECURITY, reload_labels from utils.issue_sync import sync_alerts_and_issues from utils.logging_config import setup_logging from utils.teams import notify_teams, notify_teams_severity_changes @@ -122,6 +122,13 @@ def parse_args() -> argparse.Namespace: help="Teams Incoming Webhook URL for new/reopened issue alerts (default: $TEAMS_WEBHOOK_URL). " "If not set, Teams notification is skipped.", ) + p.add_argument( + "--label-config", + default=None, + help="Path to a labels.yml file that maps logical label purposes to actual " + "GitHub label names. When omitted, the default labels.yml next to this " + "script is used (and if that file is absent, built-in defaults apply).", + ) return p.parse_args() @@ -135,8 +142,27 @@ def main() -> None: verbose = bool(args.verbose) or parse_runner_debug() setup_logging(verbose) + # Load custom label configuration (must happen before any label constant + # is resolved elsewhere, such as in issue_sync). Calling reload_labels() + # refreshes the module-level LABEL_* attributes in constants.py. + label_config_path: str | None = args.label_config + reload_labels(label_config_path) + + # If the user didn't explicitly pass --issue-label, use the (possibly + # reconfigured) scope_security label from the config file. + # Argparse captured the LABEL_SCOPE_SECURITY value at import time, so to + # honour a runtime --label-config you must: + # 1. reload the constants + # 2. detect whether the user actually supplied a different --issue-label (vs. the stale default) + # 3. if not supplied, replace the stale default with the freshly loaded label + from utils.constants import LABEL_SCOPE_SECURITY as _resolved # noqa: E402 + issue_label = str(args.issue_label) + + if issue_label == LABEL_SCOPE_SECURITY: + issue_label = _resolved + repo_full, open_alerts = load_open_alerts_from_file(args.file) - issues = gh_issue_list_by_label(repo_full, str(args.issue_label)) + issues = gh_issue_list_by_label(repo_full, issue_label) # Build severity → priority map from user input; empty by default (priority skipped). spm = parse_severity_priority_map(str(args.severity_priority_map or "")) diff --git a/github/security/sync_security_alerts.sh b/github/security/sync_security_alerts.sh index b9ddd74..8bb439a 100755 --- a/github/security/sync_security_alerts.sh +++ b/github/security/sync_security_alerts.sh @@ -21,6 +21,7 @@ REPO="" STATE="open" # open | dismissed | fixed | all OUT_FILE="alerts.json" ISSUE_LABEL="scope:security" +LABEL_CONFIG="" SEVERITY_PRIORITY_MAP="${SEVERITY_PRIORITY_MAP:-}" PROJECT_NUMBER="${PROJECT_NUMBER:-}" PROJECT_ORG="${PROJECT_ORG:-}" @@ -47,6 +48,8 @@ Options: --state open | dismissed | fixed | all (default: open) --out Output file for alerts JSON (default: alerts.json) --issue-label