diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..090690d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# CODEOWNERS — interim owners for governance and workflows +# Replace with your org/team handles when available (e.g., @libis/security @libis/privacy). + +/governance/* @ErykKul +/governance/** @ErykKul +/policies/* @ErykKul +/.github/workflows/* @ErykKul diff --git a/.github/ai-transition-sync.json b/.github/ai-transition-sync.json new file mode 100644 index 0000000..6440ff3 --- /dev/null +++ b/.github/ai-transition-sync.json @@ -0,0 +1,7 @@ +{ + "source_repo": "libis/ai-transition", + "source_ref": "main", + "synced_at": "2025-09-16T00:00:00Z", + "files_copied": [], + "upstream_commit": "unknown" +} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..a28e02c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,67 @@ + + +# @ai-tool: Copilot + +## Summary + + + +## AI Provenance (required for AI-assisted changes) + +- Prompt: +- Model: +- Date: +- Author: +- Role: provider|deployer + +## Compliance checklist + +- [ ] No secrets/PII +- [ ] Transparency notice updated (if user-facing) +- [ ] Agent logging enabled (actions/decisions logged) +- [ ] Kill-switch / feature flag present for AI features +- [ ] No prohibited practices under EU AI Act +- [ ] Human oversight retained (required if high-risk or agent mode) +- Risk classification: limited|high +- Personal data: yes|no +- DPIA: +- Automated decision-making: yes|no +- Agent mode used: yes|no +- GPAI obligations: (if Role: provider) +- Vendor GPAI compliance reviewed: (if Role: deployer) +- [ ] License/IP attestation +- Attribution: +- Oversight plan: (required if high-risk/ADM) + + +> Tip: comment '/gov' to run checks + Copilot review, and '/gov links' to preview suggested links. + +### Change-type specifics + +- Security review: or check: [ ] Security review requested (required if auth/permissions/etc.) +- Media assets changed: + - [ ] AI content labeled + - C2PA: +- UI changed: + - [ ] Accessibility review (EN 301 549/WCAG) + - Accessibility statement: +- Deploy/infra changed: + - Privacy notice: + - Lawful basis: + - Retention schedule: + - NIS2 applicability: yes|no|N/A + - Incident response plan: +- Backend/API changed: + - ASVS: or check [ ] OWASP ASVS review +- Log retention policy: +- Data paths changed: + - TDM: yes|no|N/A + - TDM compliance: + +## Tests & Risk + +- [ ] Unit/integration tests added/updated +- [ ] Security scan passed +- Rollback plan: +- Smoke test: +- [ ] Docs updated (if needed) diff --git a/.github/workflows/ai-agent.yml b/.github/workflows/ai-agent.yml new file mode 100644 index 0000000..55f13e5 --- /dev/null +++ b/.github/workflows/ai-agent.yml @@ -0,0 +1,137 @@ +# @ai-generated: true +# @ai-tool: Copilot +name: AI Governance Agent (ChatOps) + +on: + issue_comment: + types: [created] + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + respond: + name: Respond to /gov commands on PRs + if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, '/gov') }} + runs-on: ubuntu-latest + steps: + - name: Handle /gov command + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const body = context.payload.comment.body.trim(); + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue_number = context.payload.issue.number; + + const helpText = + 'AI Governance Agent commands:\\n\\n\n' + + `- /gov help — show this help\n` + + `- /gov check — scan this PR for missing governance checklist items and summarize changes\n` + + `- /gov copilot — ask GitHub Copilot to review this PR\n` + + `- /gov links — preview suggested links (governance/test runs) for the PR template\n` + + `- /gov autofill apply — auto-fill safe N/A defaults and add run links into the PR body\n` + + `- /gov — run default check, trigger Copilot review, preview links, and auto-apply autofill\n`; + + const isHelp = body.match(/^\/gov\s+help\b/i); + const isCheck = body.match(/^\/gov\s+check\b/i); + const isBare = body.match(/^\/gov\s*$/i); + + if (!isHelp && !isCheck && !isBare) { + return; // Ignore other /gov variants for now + } + + if (isHelp) { + await github.rest.issues.createComment({ owner, repo, issue_number, body: helpText }); + return; + } + + // Fetch PR details and changed files (treat bare /gov as /gov check) + const doCheck = isCheck || isBare; + const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: issue_number }); + const prBody = (pr.body || '').toString(); + const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number: issue_number, per_page: 100 }); + + // Heuristics for change types + const changedPaths = files.map(f => f.filename); + const rx = { + userUI: /(^|\/)(ui|web|frontend|public|templates)(\/|$)|(^|\/)src\/.*\.(html|tsx?|vue)$/i, + sensitive: /(^|\/)(auth|authn|authz|login|acl|permissions?|access[_-]?control|secrets?|tokens?|jwt|oauth)(\/|$)|\.(policy|rego)$/i, + infra: /(^|\/)(k8s|kubernetes|helm|charts|deploy|ops|infra|infrastructure|manifests|terraform|ansible)(\/|$)|(^|\/)dockerfile$|docker-compose\.ya?ml$|Chart\.ya?ml$/i, + backend: /(^|\/)(src|api|server|backend|app)(\/|\/.*)([^\/]+)\.(js|ts|py|rb|go|java|cs)$/i, + media: /\.(png|jpe?g|gif|webp|svg|mp4|mp3|wav|pdf)$/i, + data: /(^|\/)(data|datasets|training|notebooks|scripts)(\/|$)/i + }; + const has = (re) => changedPaths.some(p => re.test(p)); + const flags = { + userUI: has(rx.userUI), + sensitive: has(rx.sensitive), + infra: has(rx.infra), + backend: has(rx.backend), + media: has(rx.media), + data: has(rx.data) + }; + + // Simple PR body checks mirroring the reusable workflow + const missing = []; + const need = (label, ok) => { if (!ok) missing.push(label); }; + + need('Prompt', /Prompt/i.test(prBody)); + need('Model', /Model/i.test(prBody)); + need('Date', /Date/i.test(prBody)); + need('Author', /Author/i.test(prBody)); + need('[x] No secrets/PII', /\[x\].*no\s+secrets\/?pii|no\s+pii\/?secrets/i.test(prBody)); + need('Risk classification: limited|high', /Risk\s*classification:\s*(limited|high)/i.test(prBody)); + need('Personal data: yes|no', /Personal\s*data:\s*(yes|no)/i.test(prBody)); + need('Automated decision-making: yes|no', /Automated\s*decision-?making:\s*(yes|no)/i.test(prBody)); + need('Agent mode used: yes|no', /Agent\s*mode\s*used:\s*(yes|no)/i.test(prBody)); + need('Role: provider|deployer', /Role:\s*(provider|deployer)/i.test(prBody)); + + if (flags.userUI) { + need('[x] Transparency notice updated', /\[x\].*transparency\s+notice/i.test(prBody)); + need('Accessibility statement: ', /Accessibility\s*statement:\s*(https?:\/\/|N\/?A)/i.test(prBody)); + } + if (flags.media) { + need('[x] AI content labeled', /\[x\].*ai\s*content\s*labeled/i.test(prBody)); + need('C2PA: ', /C2PA:\s*(https?:\/\/|N\/?A)/i.test(prBody)); + } + if (flags.infra) { + need('Privacy notice: ', /Privacy\s*notice:\s*(https?:\/\/)/i.test(prBody)); + need('Lawful basis: ', /Lawful\s*basis:\s*([A-Za-z]+|N\/?A)/i.test(prBody)); + need('Retention schedule: ', /Retention\s*schedule:\s*(https?:\/\/|N\/?A)/i.test(prBody)); + } + if (flags.backend) { + need('[x] OWASP ASVS review or ASVS: ', /\[x\].*owasp\s*asvs|ASVS:\s*(https?:\/\/)/i.test(prBody)); + } + + // Build a concise response + const bullet = (b) => `- ${b}`; + const filesList = changedPaths.slice(0, 50).map(bullet).join('\n'); + const missingList = missing.length ? missing.map(bullet).join('\n') : '- None (looks good)'; + const flagsList = Object.entries(flags).filter(([,v]) => v).map(([k]) => ` + - ${k}`).join('') || '\n - none detected'; + + const reply = + '### Governance Agent Report\\n\\n\n' + + `PR: #${issue_number} by @${pr.user.login}\n\n` + + `Changed files (${changedPaths.length}):\n${filesList}\n\n` + + `Detected change types:${flagsList}\n\n` + + `Missing or incomplete items:\n${missingList}\n\n` + + `Tip: Use the PR template fields to satisfy these checks.\n\n` + + `Run /gov help for commands. Also try: /gov links and /gov autofill apply.`; + + if (doCheck) { + await github.rest.issues.createComment({ owner, repo, issue_number, body: reply }); + } + + // If the command was bare /gov, also trigger Copilot review, links preview, and auto-apply autofill + if (isBare) { + await github.rest.issues.createComment({ owner, repo, issue_number, body: '/gov copilot' }); + // And trigger auto-links preview so contributors can quickly fill PR fields + await github.rest.issues.createComment({ owner, repo, issue_number, body: '/gov links' }); + // Finally, auto-apply link autofill (safe defaults + run links) + await github.rest.issues.createComment({ owner, repo, issue_number, body: '/gov autofill apply' }); + } diff --git a/.github/workflows/ai-governance.yml b/.github/workflows/ai-governance.yml new file mode 100644 index 0000000..cc46689 --- /dev/null +++ b/.github/workflows/ai-governance.yml @@ -0,0 +1,542 @@ +# @ai-generated: true +# @ai-tool: Copilot +name: AI Governance Checks + +on: + workflow_call: + inputs: + run_markdownlint: + required: false + type: boolean + default: true + run_gitleaks: + required: false + type: boolean + default: true + run_dependency_review: + required: false + type: boolean + default: true + run_scancode: + required: false + type: boolean + default: true + run_sbom: + required: false + type: boolean + default: true + run_codeql: + required: false + type: boolean + default: false + lint_command: + required: false + type: string + default: '' + test_command: + required: false + type: string + default: '' + require_ui_transparency: + required: false + type: boolean + default: true + require_dpia_for_user_facing: + required: false + type: boolean + default: true + require_eval_for_high_risk: + required: false + type: boolean + default: false + enable_post_merge_reminders: + required: false + type: boolean + default: true + +permissions: + contents: read + pull-requests: write + issues: write + security-events: write + +jobs: + policy_checks: + name: Policy checks (provenance, risk notes) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Compute changed files + shell: bash + run: | + base=$(jq -r '.pull_request.base.sha' "$GITHUB_EVENT_PATH") + head=$(jq -r '.pull_request.head.sha' "$GITHUB_EVENT_PATH") + git fetch --no-tags --depth=1 origin "$base" || true + git diff --name-only "$base" "$head" > changed_files.txt || true + echo "Changed files:"; cat changed_files.txt || true + # Heuristic: user-facing change if paths include common web/ui dirs or templates + if grep -Eiq '(^|/)(ui|web|frontend|public|templates|src/.+\.(html|tsx?|vue))$' changed_files.txt; then + echo "user_facing_change=true" >> "$GITHUB_ENV" + else + echo "user_facing_change=false" >> "$GITHUB_ENV" + fi + # Sensitive modules (authz/authn/permissions/secrets) + if grep -Eiq '(^|/)(auth|authn|authz|login|acl|permissions?|access[_-]?control|secrets?|tokens?|jwt|oauth)(/|$)|\.(policy|rego)$' changed_files.txt; then + echo "sensitive_modules=true" >> "$GITHUB_ENV" + else + echo "sensitive_modules=false" >> "$GITHUB_ENV" + fi + # Media assets changed (content provenance / labeling) + if grep -Eiq '\.(png|jpe?g|gif|webp|svg|mp4|mp3|wav|pdf)$' changed_files.txt; then + echo "media_change=true" >> "$GITHUB_ENV" + else + echo "media_change=false" >> "$GITHUB_ENV" + fi + # Infrastructure / deploy manifests changed + if grep -Eiq '(^|/)(k8s|kubernetes|helm|charts|deploy|ops|infra|infrastructure|manifests|terraform|ansible)(/|$)|(^|/)dockerfile$|docker-compose\.ya?ml$|Chart\.ya?ml$' changed_files.txt; then + echo "infra_change=true" >> "$GITHUB_ENV" + else + echo "infra_change=false" >> "$GITHUB_ENV" + fi + # Backend/API code changed + if grep -Eiq '(^|/)(src|api|server|backend|app)(/|/.*)([^/]+)\.(js|ts|py|rb|go|java|cs)$' changed_files.txt; then + echo "backend_change=true" >> "$GITHUB_ENV" + else + echo "backend_change=false" >> "$GITHUB_ENV" + fi + # Data/TDM related paths + if grep -Eiq '(^|/)(data|datasets|training|notebooks|scripts)(/|$)' changed_files.txt; then + echo "data_change=true" >> "$GITHUB_ENV" + else + echo "data_change=false" >> "$GITHUB_ENV" + fi + - name: Check PR provenance fields + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Prefer live PR body via API to avoid stale event payloads + pr_number=$(jq -r '.pull_request.number // empty' "$GITHUB_EVENT_PATH") + api="${GITHUB_API_URL:-https://api.github.com}" + repo="$GITHUB_REPOSITORY" + body=$(curl -sSf -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/vnd.github+json" \ + "$api/repos/$repo/pulls/$pr_number" | jq -r '.body // empty' || true) + # Fallback to event payload if API is unavailable + if [ -z "$body" ]; then + body=$(jq -r '.pull_request.body // ""' "$GITHUB_EVENT_PATH") + fi + # Normalize line endings (strip CR) + body=$(printf "%s" "$body" | sed 's/\r$//') + missing=0 + for key in "Prompt" "Model" "Date" "Author"; do + echo "$body" | grep -qi "$key" || { echo "::error::Missing $key in PR body"; missing=1; } + done + # Date must be strict ISO-8601 UTC Z; accept '-' or '*' bullets + echo "$body" | grep -Eiq '^[[:space:]]*[-*]\s*Date:\s*20[0-9]{2}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z(\s*()\s*)?$' || { + echo "::error::Date must be a real UTC ISO-8601 timestamp (e.g., 2025-09-12T10:21:36Z)."; missing=1; } + # Reject placeholders and templating/backticks in key fields + echo "$body" | grep -Eiq '\\$\{|\$\(|`|, \${...}, \$(...), or backticks with concrete values or N/A where allowed."; missing=1; } + if [ $missing -ne 0 ]; then + echo "::error::Provenance fields missing in PR body (Prompt/Model/Date/Author)."; exit 1; + fi + - name: Require explicit No PII/Secrets checkbox + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + pr_number=$(jq -r '.pull_request.number // empty' "$GITHUB_EVENT_PATH") + api="${GITHUB_API_URL:-https://api.github.com}" + repo="$GITHUB_REPOSITORY" + body=$(curl -sSf -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/vnd.github+json" \ + "$api/repos/$repo/pulls/$pr_number" | jq -r '.body // empty' || true) + [ -n "$body" ] || body=$(jq -r '.pull_request.body // ""' "$GITHUB_EVENT_PATH") + body=$(printf "%s" "$body" | sed 's/\r$//') + # Accept either '- [x] No secrets/PII' or '[x] No secrets/PII' (case-insensitive) + echo "$body" | grep -Eqi "\[x\].*no\s+secrets/?pii|no\s+pii/?secrets" || { + echo "::error::Please confirm '[x] No secrets/PII' in the PR checklist."; exit 1; + } + - name: Additional compliance (transparency, DPIA, logging, kill-switch, risk classification) + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + pr_number=$(jq -r '.pull_request.number // empty' "$GITHUB_EVENT_PATH") + api="${GITHUB_API_URL:-https://api.github.com}" + repo="$GITHUB_REPOSITORY" + body=$(curl -sSf -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/vnd.github+json" \ + "$api/repos/$repo/pulls/$pr_number" | jq -r '.body // empty' || true) + [ -n "$body" ] || body=$(jq -r '.pull_request.body // ""' "$GITHUB_EVENT_PATH") + body=$(printf "%s" "$body" | sed 's/\r$//') + # If user-facing changes and transparency required, enforce checkbox + if [ "${{ inputs.require_ui_transparency }}" = "true" ] && [ "$user_facing_change" = "true" ]; then + echo "$body" | grep -Eqi "\[x\].*transparency\s+notice" || { + echo "::error::For user-facing changes, check '[x] Transparency notice updated'"; exit 1; } + fi + # DPIA acknowledgement (link or N/A) for user-facing or personal-data + if [ "${{ inputs.require_dpia_for_user_facing }}" = "true" ] && [ "$user_facing_change" = "true" ]; then + echo "$body" | grep -Eqi "DPIA:\s*(https?://|N/?A)" || { + echo "::error::Add 'DPIA: ' line to PR body for user-facing changes"; exit 1; } + fi + # Logging & kill-switch acknowledgements + echo "$body" | grep -Eqi "\[x\].*agent\s+logging" || { echo "::error::Check '[x] Agent logging enabled]'"; exit 1; } + echo "$body" | grep -Eqi "\[x\].*(kill\-switch|feature\s+flag)" || { echo "::error::Check '[x] Kill-switch / feature flag present]'"; exit 1; } + # Risk classification (limited/high) + echo "$body" | grep -Eqi "Risk\s+classification:\s*(limited|high)" || { echo "::error::Add 'Risk classification: limited|high' to PR body"; exit 1; } + risk=$(echo "$body" | sed -n 's/.*Risk[[:space:]]\{1,\}classification:[[:space:]]*\(limited\|high\).*/\1/ip' | head -n1 | tr '[:upper:]' '[:lower:]') + # Personal data and ADM (automated decision-making) + echo "$body" | grep -Eqi "Personal\s*data:\s*(yes|no)" || { echo "::error::Add 'Personal data: yes|no'"; exit 1; } + personal=$(echo "$body" | sed -n 's/.*Personal[[:space:]]*data:[[:space:]]*\(yes\|no\).*/\1/ip' | head -n1 | tr '[:upper:]' '[:lower:]') + echo "$body" | grep -Eqi "Automated\s*decision\-?making:\s*(yes|no)" || { echo "::error::Add 'Automated decision-making: yes|no'"; exit 1; } + adm=$(echo "$body" | sed -n 's/.*Automated[[:space:]]*decision-\{0,1\}making:[[:space:]]*\(yes\|no\).*/\1/ip' | head -n1 | tr '[:upper:]' '[:lower:]') + # Agent mode used + echo "$body" | grep -Eqi "Agent\s*mode\s*used:\s*(yes|no)" || { echo "::error::Add 'Agent mode used: yes|no'"; exit 1; } + agentmode=$(echo "$body" | sed -n 's/.*Agent[[:space:]]*mode[[:space:]]*used:[[:space:]]*\(yes\|no\).*/\1/ip' | head -n1 | tr '[:upper:]' '[:lower:]') + # Provider vs deployer + echo "$body" | grep -Eqi "Role:\s*(provider|deployer)" || { echo "::error::Add 'Role: provider|deployer'"; exit 1; } + role=$(echo "$body" | sed -n 's/.*Role:[[:space:]]*\(provider\|deployer\).*/\1/ip' | head -n1 | tr '[:upper:]' '[:lower:]') + if [ "$role" = "provider" ]; then + echo "$body" | grep -Eqi "GPAI\s*obligations:\s*(https?://|N/?A)" || { echo "::error::Add 'GPAI obligations: '"; exit 1; } + fi + if [ "$role" = "deployer" ]; then + echo "$body" | grep -Eqi "Vendor\s*GPAI\s*compliance\s*reviewed:\s*(https?://|N/?A)" || { echo "::error::Add 'Vendor GPAI compliance reviewed: '"; exit 1; } + fi + # Prohibited practices attestation + echo "$body" | grep -Eqi "\[x\].*no\s+prohibited\s+practices" || { echo "::error::Confirm '[x] No prohibited practices under EU AI Act'"; exit 1; } + # Human oversight if agent mode used or high risk + if [ "$agentmode" = "yes" ] || [ "$risk" = "high" ]; then + echo "$body" | grep -Eqi "\[x\].*human\s+oversight" || { echo "::error::Check '[x] Human oversight retained'"; exit 1; } + fi + # If automated decision-making is yes, require high risk classification and oversight plan + if [ "$adm" = "yes" ]; then + [ "$risk" = "high" ] || { echo "::error::Automated decision-making implies 'Risk classification: high'"; exit 1; } + echo "$body" | grep -Eqi "Oversight\s*plan:\s*(https?://)" || { echo "::error::Add 'Oversight plan: ' for high-risk/ADM"; exit 1; } + fi + # If personal data yes, require DPIA link (even if not user-facing) + if [ "$personal" = "yes" ]; then + echo "$body" | grep -Eqi "DPIA:\s*(https?://)" || { echo "::error::Provide 'DPIA: ' when personal data is processed"; exit 1; } + fi + # High risk: require rollback plan and smoke test + if [ "$risk" = "high" ]; then + echo "$body" | grep -Eqi "Rollback\s*plan:\s*.+" || { echo "::error::Add 'Rollback plan: ' for high-risk changes"; exit 1; } + echo "$body" | grep -Eqi "Smoke\s*test:\s*(https?://)" || { echo "::error::Add 'Smoke test: ' for high-risk changes"; exit 1; } + fi + # Evaluation results: optionally enforce for high-risk + if [ "${{ inputs.require_eval_for_high_risk }}" = "true" ] && [ "$risk" = "high" ]; then + echo "$body" | grep -Eqi "Eval\s*set:\s*(https?://)" || { echo "::error::Add 'Eval set: ' for high-risk"; exit 1; } + # Expect 'Error rate: 1.5%' style; must be <= 2 + er=$(echo "$body" | sed -n 's/.*Error[[:space:]]*rate:[[:space:]]*\([0-9]*\.?[0-9]*\)%.*/\1/p' | head -n1) + if [ -z "$er" ]; then echo "::error::Add 'Error rate: ' for high-risk"; exit 1; fi + awk -v er="$er" 'BEGIN { if (er+0 > 2.0) { exit 1 } }' || { echo "::error::Error rate must be <= 2% for high-risk"; exit 1; } + else + # Non-blocking warning if error rate declared > 2% + er=$(echo "$body" | sed -n 's/.*Error[[:space:]]*rate:[[:space:]]*\([0-9]*\.?[0-9]*\)%.*/\1/p' | head -n1) + if [ -n "$er" ]; then awk -v er="$er" 'BEGIN { if (er+0 > 2.0) { print "::warning::Declared error rate > 2%"; } }'; fi + fi + # Prompt injection mitigation for agented backend/data changes + if [ "$agentmode" = "yes" ] && { [ "$backend_change" = "true" ] || [ "$data_change" = "true" ]; }; then + echo "$body" | grep -Eqi "\[x\].*untrusted\s*input\s*sanitized" || { echo "::error::Confirm '[x] Untrusted input sanitized' for agent mode with backend/data changes"; exit 1; } + fi + # License/IP attestation & attribution + echo "$body" | grep -Eqi "\[x\].*license/?ip\s*attestation" || { echo "::error::Confirm '[x] License/IP attestation'"; exit 1; } + echo "$body" | grep -Eqi "Attribution:\s*(https?://|N/?A)" || { echo "::error::Add 'Attribution: ' if applicable"; exit 1; } + # Sensitive modules require security review + if [ "$sensitive_modules" = "true" ]; then + echo "$body" | grep -Eqi "\[x\].*security\s+review|Security\s*review:\s*(https?://)" || { echo "::error::Sensitive modules changed; add '[x] Security review requested' or 'Security review: '"; exit 1; } + fi + # Media assets: require AI content labeling + C2PA link or N/A + if [ "$media_change" = "true" ]; then + echo "$body" | grep -Eqi "\[x\].*ai\s*content\s*labeled" || { echo "::error::Media changed; confirm '[x] AI content labeled'"; exit 1; } + echo "$body" | grep -Eqi "C2PA:\s*(https?://|N/?A)" || { echo "::error::Add 'C2PA: ' for media provenance"; exit 1; } + fi + # UI/Accessibility: require accessibility review + statement link when UI changed + if [ "$user_facing_change" = "true" ]; then + echo "$body" | grep -Eqi "\[x\].*accessibility\s+(review|check)" || { echo "::error::UI changed; confirm '[x] Accessibility review (EN 301 549/WCAG)'"; exit 1; } + echo "$body" | grep -Eqi "Accessibility\s*statement:\s*(https?://|N/?A)" || { echo "::error::Add 'Accessibility statement: '"; exit 1; } + fi + # Infra/deploy changes: require privacy notice, lawful basis, retention schedule, NIS2 applicability and incident plan if yes + if [ "$infra_change" = "true" ]; then + echo "$body" | grep -Eqi "Privacy\s*notice:\s*(https?://)" || { echo "::error::Add 'Privacy notice: ' for deploying changes"; exit 1; } + echo "$body" | grep -Eqi "Lawful\s*basis:\s*[A-Za-z]+|N/?A" || { echo "::error::Add 'Lawful basis: '"; exit 1; } + echo "$body" | grep -Eqi "Retention\s*schedule:\s*(https?://|N/?A)" || { echo "::error::Add 'Retention schedule: '"; exit 1; } + echo "$body" | grep -Eqi "NIS2\s*applicability:\s*(yes|no|N/?A)" || { echo "::error::Add 'NIS2 applicability: yes|no|N/A'"; exit 1; } + nis=$(echo "$body" | sed -n 's/.*NIS2[[:space:]]*applicability:[[:space:]]*\(yes\|no\|N\/?A\).*/\1/ip' | head -n1 | tr '[:upper:]' '[:lower:]') + if [ "$nis" = "yes" ]; then + echo "$body" | grep -Eqi "Incident\s*response\s*plan:\s*(https?://)" || { echo "::error::Provide 'Incident response plan: ' for NIS2"; exit 1; } + fi + fi + # Backend/API changes: require OWASP ASVS review link or checkbox + if [ "$backend_change" = "true" ]; then + echo "$body" | grep -Eqi "\[x\].*owasp\s*asvs|ASVS:\s*(https?://)" || { echo "::error::Backend/API changed; confirm '[x] OWASP ASVS review' or add 'ASVS: '"; exit 1; } + fi + # Log retention: if personal data yes, high risk, or infra_change, require a log retention policy link or N/A + if [ "$personal" = "yes" ] || [ "$risk" = "high" ] || [ "$infra_change" = "true" ]; then + echo "$body" | grep -Eqi "Log\s*retention\s*policy:\s*(https?://|N/?A)" || { echo "::error::Add 'Log retention policy: '"; exit 1; } + fi + # TDM compliance if data paths changed + if [ "$data_change" = "true" ]; then + echo "$body" | grep -Eqi "TDM:\s*(yes|no|N/?A)" || { echo "::error::Add 'TDM: yes|no|N/A'"; exit 1; } + tdm=$(echo "$body" | sed -n 's/.*TDM:[[:space:]]*\(yes\|no\|N\/?A\).*/\1/ip' | head -n1 | tr '[:upper:]' '[:lower:]') + if [ "$tdm" = "yes" ]; then + echo "$body" | grep -Eqi "TDM\s*compliance:\s*(https?://)" || { echo "::error::Provide 'TDM compliance: ' (dataset/source register)"; exit 1; } + fi + fi + - name: Auto-label PR as ai-assisted + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + if (!pr) return; + const labels = (pr.labels || []).map(l => l.name); + if (!labels.includes('ai-assisted')) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: ['ai-assisted'] + }); + } + const body = pr.body || ''; + const toAdd = []; + if (/Risk\s*classification:\s*high/i.test(body)) toAdd.push('high-risk'); + if (/Personal\s*data:\s*yes/i.test(body)) toAdd.push('personal-data'); + if (/Agent\s*mode\s*used:\s*yes/i.test(body)) toAdd.push('agent-mode'); + const roleMatch = body.match(/Role:\s*(provider|deployer)/i); + if (roleMatch) toAdd.push(roleMatch[1].toLowerCase()); + if (/Security\s*review:/i.test(body) || /\[x\].*security\s+review/i.test(body)) toAdd.push('security-review'); + if (/\[x\].*owasp\s*asvs|ASVS:/i.test(body)) toAdd.push('asvs'); + if (/NIS2\s*applicability:\s*yes/i.test(body)) toAdd.push('nis2'); + // Optionally infer change-type labels here (non-blocking) + if (toAdd.length) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: toAdd + }); + } + - name: Require two approvals for high-risk changes + if: ${{ github.event_name == 'pull_request' }} + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + if (!pr) return; + const highRisk = /Risk\s*classification:\s*high/i.test(pr.body || ''); + if (!highRisk) return; + const { data: reviews } = await github.rest.pulls.listReviews({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + per_page: 100 + }); + const approvals = new Set(reviews.filter(r => r.state === 'APPROVED').map(r => r.user.login)); + if (approvals.size < 2) { + core.setFailed(`High-risk changes require >= 2 approvals. Current unique approvals: ${approvals.size}`); + } + - name: Comment with guidance (on failure) + if: failure() + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + if (!pr) { core.info('No PR payload; skipping comment'); return; } + const body = + '### AI Governance checks failed\n' + + '\n' + + 'Please fix the following before re-running checks:\n' + + '\n' + + '- Ensure PR body includes provenance fields:\n' + + ' - Prompt\n' + + ' - Model\n' + + ' - Date\n' + + ' - Author\n' + + ' - [x] No secrets/PII (checkbox)\n' + + '- Complete compliance checklist items required for your change type (transparency notice, DPIA, logging, kill-switch, risk classification, human oversight, security review, vendor GPAI review).\n' + + '- Add a rollback note if the change is risky (authz, data export, evaluation logic, etc.).\n' + + '\n' + + 'Helpful links:\n' + + '- PR template (provenance): https://github.com/libis/ai-transition/blob/main/.github/pull_request_template.md\n' + + '- Risk mitigation matrix: https://github.com/libis/ai-transition/blob/main/governance/risk_mitigation_matrix.md\n' + + '- Reusable governance workflow: https://github.com/libis/ai-transition/blob/main/.github/workflows/ai-governance.yml\n' + + '\n' + + 'After edits, push updates or re-run the workflow to validate.\n'; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body + }); + - name: Risk/rollback note present (non-blocking advisory) + shell: bash + continue-on-error: true + run: | + pr_number=$(jq -r '.pull_request.number // empty' "$GITHUB_EVENT_PATH") + api="${GITHUB_API_URL:-https://api.github.com}" + repo="$GITHUB_REPOSITORY" + body=$(curl -sSf -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "Accept: application/vnd.github+json" \ + "$api/repos/$repo/pulls/$pr_number" | jq -r '.body // empty' || true) + [ -n "$body" ] || body=$(jq -r '.pull_request.body // ""' "$GITHUB_EVENT_PATH") + body=$(printf "%s" "$body" | sed 's/\r$//') + echo "$body" | grep -Eqi "rollback|risk|incident" || echo "::warning::Consider adding a rollback note and risk summary for risky changes." + + post_merge_reminders: + name: Post-merge compliance reminders + if: ${{ github.event_name == 'push' && inputs.enable_post_merge_reminders }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - id: compute + name: Compute changed files (push) + shell: bash + run: | + before=$(jq -r '.before // empty' "$GITHUB_EVENT_PATH") + after=$(jq -r '.after // env.GITHUB_SHA' "$GITHUB_EVENT_PATH") + if [ -n "$before" ]; then + git fetch --no-tags --depth=1 origin "$before" || true + git diff --name-only "$before" "$after" > changed_files.txt || true + else + git diff --name-only HEAD~1 HEAD > changed_files.txt || true + fi + if grep -Eiq '(^|/)(ui|web|frontend|public|templates|src/.+\.(html|tsx?|vue))$' changed_files.txt; then + echo "user_facing_change=true" >> "$GITHUB_OUTPUT" + else + echo "user_facing_change=false" >> "$GITHUB_OUTPUT" + fi + - name: Create follow-up issue for UI transparency/privacy updates + if: ${{ steps.compute.outputs.user_facing_change == 'true' }} + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const title = + 'Post-deploy AI compliance checklist (${process.env.GITHUB_SHA.slice(0,7)})\n'; + const body = + 'This is an automated reminder for recent user-facing changes.\\n\\n\n' + + `Checklist:\n` + + `- [ ] Update transparency notice in UI (AI disclosure)\n` + + `- [ ] Update privacy notice and accessibility statements if applicable\n` + + `- [ ] Verify kill-switch / feature flag works in production\n` + + `- [ ] Monitor error rates and agent logs for 7 days\n` + + `- [ ] Archive SBOM and ScanCode artifacts in release or internal registry\n`; + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title, + body, + labels: ['post-deploy-compliance'] + }); + + + markdownlint: + if: ${{ inputs.run_markdownlint }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '18' + - name: Install markdownlint-cli + run: npm install -g markdownlint-cli@0.39.0 + - name: Lint Markdown + run: | + markdownlint "**/*.md" --ignore node_modules || (echo "::error::Markdown lint errors found"; exit 1) + + gitleaks: + if: ${{ inputs.run_gitleaks }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: gitleaks scan + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: --redact + + dependency_review: + if: ${{ inputs.run_dependency_review && github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + steps: + - name: Review dependencies for vulnerabilities & licenses + uses: actions/dependency-review-action@v4 + with: + allow-licenses: 'MIT, BSD-2-Clause, BSD-3-Clause, Apache-2.0, ISC, MPL-2.0' + fail-on-severity: critical + + scancode: + if: ${{ inputs.run_scancode }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install ScanCode + shell: bash + run: | + python -m pip install --upgrade pip + pip install scancode-toolkit + - name: Run ScanCode (JSON) + shell: bash + run: | + scancode --json-pp scancode.json --license --copyright --info . || true + test -s scancode.json || { echo '{}' > scancode.json; } + - name: Upload ScanCode report + uses: actions/upload-artifact@v4 + with: + name: scancode-report + path: scancode.json + + sbom: + if: ${{ inputs.run_sbom }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Generate SBOM (SPDX) + uses: anchore/sbom-action@v0 + with: + path: . + format: spdx-json + output-file: sbom.spdx.json + - name: Upload SBOM artifact + uses: actions/upload-artifact@v4 + with: + name: sbom-spdx + path: sbom.spdx.json + + codeql: + if: ${{ inputs.run_codeql }} + permissions: + actions: read + contents: read + security-events: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: javascript, typescript, python, ruby, go, java, cpp + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + + lint: + if: ${{ inputs.lint_command != '' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run linter + run: ${{ inputs.lint_command }} + + tests: + if: ${{ inputs.test_command != '' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run tests + run: ${{ inputs.test_command }} diff --git a/.github/workflows/code-review-agent.yml b/.github/workflows/code-review-agent.yml new file mode 100644 index 0000000..2ab4b05 --- /dev/null +++ b/.github/workflows/code-review-agent.yml @@ -0,0 +1,153 @@ + # @ai-generated: true + # @ai-tool: Copilot +name: AI Code Review Agent (Python) + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +jobs: + python_review: + if: ${{ github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Determine changed Python files + id: diff + shell: bash + run: | + if ! command -v jq >/dev/null 2>&1; then sudo apt-get update && sudo apt-get install -y jq; fi + base=$(jq -r '.pull_request.base.sha' "$GITHUB_EVENT_PATH") + head=$(jq -r '.pull_request.head.sha' "$GITHUB_EVENT_PATH") + git fetch --no-tags --depth=1 origin "$base" || true + git diff --name-only "$base" "$head" | grep -E '\.py$' > py_changed.txt || true + count=$(wc -l < py_changed.txt | tr -d ' ') + echo "Changed Python files ($count):" + cat py_changed.txt || true + if [ "$count" -eq 0 ]; then + echo "has_py=false" >> "$GITHUB_OUTPUT" + else + echo "has_py=true" >> "$GITHUB_OUTPUT" + fi + + - name: Set up Python + if: steps.diff.outputs.has_py == 'true' + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install linters (ruff, bandit) + if: steps.diff.outputs.has_py == 'true' + run: | + python -m pip install --upgrade pip + pip install ruff==0.5.7 bandit==1.7.9 + + - name: Run Ruff (style/quality) + if: steps.diff.outputs.has_py == 'true' + shell: bash + run: | + mapfile -t files < py_changed.txt || true + if [ ${#files[@]} -gt 0 ]; then + ruff check "${files[@]}" --output-format=json > ruff.json || true + else + echo '[]' > ruff.json + fi + + - name: Run Bandit (security) + if: steps.diff.outputs.has_py == 'true' + shell: bash + run: | + mapfile -t files < py_changed.txt || true + if [ ${#files[@]} -gt 0 ]; then + bandit -q -f json -o bandit.json "${files[@]}" || true + else + echo '{"results":[]}' > bandit.json + fi + + - name: Comment review summary + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue_number = context.payload.pull_request.number; + + function readJsonSafe(path, fallback) { + try { return JSON.parse(fs.readFileSync(path, 'utf8')); } catch { return fallback; } + } + + const hasPy = fs.existsSync('py_changed.txt') && fs.readFileSync('py_changed.txt','utf8').trim().length > 0; + const pyFiles = hasPy ? fs.readFileSync('py_changed.txt','utf8').trim().split('\n') : []; + const ruff = readJsonSafe('ruff.json', []); + const bandit = readJsonSafe('bandit.json', { results: [] }); + + // Normalize Ruff findings + const ruffByFile = new Map(); + for (const f of ruff) { + // Ruff json entries may include filename and diagnostics array or directly as list; handle both + if (f && f.filename && Array.isArray(f.diagnostics)) { + for (const d of f.diagnostics) { + const k = f.filename; + const arr = ruffByFile.get(k) || []; + arr.push({ + line: d.range?.start?.line ?? d.location?.row ?? 0, + col: d.range?.start?.column ?? d.location?.column ?? 0, + code: d.code || d.rule || 'RUFF', + msg: d.message || '' + }); + ruffByFile.set(k, arr); + } + } else if (f && f.filename && f.rule && f.message) { + const arr = ruffByFile.get(f.filename) || []; + arr.push({ line: f.location?.row ?? 0, col: f.location?.column ?? 0, code: f.rule, msg: f.message }); + ruffByFile.set(f.filename, arr); + } + } + + // Normalize Bandit findings + const banditByFile = new Map(); + for (const r of bandit.results || []) { + const fn = r.filename || 'unknown'; + const arr = banditByFile.get(fn) || []; + arr.push({ line: r.line_number || 0, sev: r.issue_severity || 'MEDIUM', conf: r.issue_confidence || 'MEDIUM', msg: r.issue_text || '' }); + banditByFile.set(fn, arr); + } + + const mk = (arr) => arr.map(x => `- L${x.line}${x.col?':C'+x.col:''} ${x.code? '['+x.code+'] ':''}${x.msg}`).join('\n'); + const mkb = (arr) => arr.map(x => `- L${x.line} [${x.sev}/${x.conf}] ${x.msg}`).join('\n'); + + const ruffCount = Array.from(ruffByFile.values()).reduce((a,b)=>a+b.length,0); + const banditCount = Array.from(banditByFile.values()).reduce((a,b)=>a+b.length,0); + + let body = + '### Code Review Agent (Python)\\n\n'; + if (!hasPy) { + body += `No Python files changed. Skipping analysis.`; + } else { + body += `Analyzed ${pyFiles.length} Python file(s).\n\n`; + body += `Ruff findings: ${ruffCount}\n`; + for (const [file, items] of ruffByFile.entries()) { + body += `\n${file}\n${mk(items)}\n`; + } + body += `\nBandit findings: ${banditCount}\n`; + for (const [file, items] of banditByFile.entries()) { + body += `\n${file}\n${mkb(items)}\n`; + } + if (ruffCount === 0 && banditCount === 0) { + body += `\nNo issues found. ✅`; + } else { + body += `\nNote: This is advisory and does not block the PR. Consider addressing issues above.`; + } + } + + await github.rest.issues.createComment({ owner, repo, issue_number, body }); diff --git a/.github/workflows/copilot-pr-review.yml b/.github/workflows/copilot-pr-review.yml new file mode 100644 index 0000000..bb4caad --- /dev/null +++ b/.github/workflows/copilot-pr-review.yml @@ -0,0 +1,124 @@ +# @ai-generated: true +# @ai-tool: GitHub Copilot +name: Copilot PR Review (on-demand) + +on: + issue_comment: + types: [created] + +permissions: + contents: read + pull-requests: write + +jobs: + copilot-review: + name: Generate Copilot review + # Run only on PRs and only when explicitly asked + if: >- + ${{ github.event.issue.pull_request && + (startsWith(github.event.comment.body, '/gov copilot') || + startsWith(github.event.comment.body, '/copilot review')) }} + runs-on: ubuntu-latest + steps: + - name: Build PR context for Copilot + id: ctx + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue_number = context.payload.issue.number; + + const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: issue_number }); + const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number: issue_number, per_page: 100 }); + + const truncate = (s, n) => (s ? (s.length > n ? s.slice(0, n) + '…' : s) : ''); + const safeLines = (s, maxLines = 200) => (s || '').split('\n').slice(0, maxLines).join('\n'); + + const fileSummaries = files.map(f => `- ${f.filename} (+${f.additions}/-${f.deletions}, ${f.status}${f.changes ? ", ~"+f.changes+" lines" : ''})`).join('\n'); + const diffs = files.slice(0, 10).map(f => { + const patch = safeLines(truncate(f.patch || '', 6000), 250); + return `--- ${f.filename} (${f.status}, +${f.additions}/-${f.deletions})\n${patch}`; + }).join('\n\n'); + + const prBody = truncate(pr.body || '', 3000); + const prompt = + 'You are GitHub Copilot. Review the following Pull Request and provide a concise, helpful code review.\\n\\n\n'+ + `Please include:\n`+ + `- A 2-5 sentence summary of the changes.\n`+ + `- Potential bugs, security issues, or edge cases (with reasons).\n`+ + `- Testing gaps or scenarios to add.\n`+ + `- Style or clarity suggestions.\n`+ + `- Any breaking changes or migration notes.\n\n`+ + `Use short bullet points, reference files and line ranges when relevant, and be factual. If nothing notable, say "No significant issues found."\n\n`+ + `PR Title: ${pr.title}\n`+ + `Author: @${pr.user.login}\n`+ + `Base: ${pr.base.ref} -> Head: ${pr.head.ref}\n`+ + `PR Description (truncated):\n${prBody}\n\n`+ + `Changed files (${files.length}):\n${fileSummaries}\n\n`+ + `Sample diffs (first up to 10 files, truncated for length):\n\n${diffs}`; + + core.setOutput('prompt', prompt); + // Also write prompt to a workspace file to avoid shell quoting issues + const fs = require('fs'); + const path = require('path'); + const promptPath = path.join(process.cwd(), 'copilot_prompt.txt'); + fs.writeFileSync(promptPath, prompt, { encoding: 'utf8' }); + core.setOutput('prompt_path', promptPath); + + - name: Install gh Copilot extension + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh --version + gh extension install github/gh-copilot || gh extension upgrade github/gh-copilot || true + + - name: Ask Copilot for review + id: ask + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + REVIEW_FILE="review.md" + FINAL_FILE="final.md" + + PROMPT_FILE="copilot_prompt.txt" + # Ensure prompt file exists + if [ ! -f "$PROMPT_FILE" ]; then + echo "::error::Prompt file not found at $PROMPT_FILE"; exit 1; + fi + + # Try Copilot; if unavailable, produce a friendly fallback + if gh extension list | grep -q "github/gh-copilot"; then + if gh copilot --help > /dev/null 2>&1; then + if ! gh copilot --plain -p "$(cat "$PROMPT_FILE")" > "$REVIEW_FILE" 2>/tmp/copilot.err; then + printf "%s\n" "GitHub Copilot couldn't generate a review (perhaps not enabled for this org/repo)." > "$REVIEW_FILE" + printf "\n%s\n" "Tip: Ensure GitHub Copilot is enabled for Pull Requests in your organization or repository." >> "$REVIEW_FILE" + fi + else + echo "GitHub Copilot CLI not available on runner." > "$REVIEW_FILE" + fi + else + echo "GitHub Copilot extension not installed or unavailable." > "$REVIEW_FILE" + fi + + { + echo "### GitHub Copilot PR Review" + echo + cat "$REVIEW_FILE" + echo + echo "_Generated by GitHub Copilot. Please verify important suggestions before applying._" + } > "$FINAL_FILE" + + echo "final_path=$FINAL_FILE" >> "$GITHUB_OUTPUT" + + - name: Post review as PR comment + if: always() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + run: | + FILE="${{ steps.ask.outputs.final_path }}" + if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then FILE="final.md"; fi + gh pr comment "$PR_NUMBER" -F "$FILE" diff --git a/.github/workflows/gov-review.yml b/.github/workflows/gov-review.yml new file mode 100644 index 0000000..907b908 --- /dev/null +++ b/.github/workflows/gov-review.yml @@ -0,0 +1,206 @@ +# @ai-generated: true +# @ai-tool: Copilot +name: Governance Reports Review (/gov) + +on: + issue_comment: + types: [created] + +permissions: + contents: read + actions: read + issues: write + pull-requests: write + +jobs: + review: + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body || '', '/gov') }} + runs-on: ubuntu-latest + steps: + - name: Parse command + id: parse + uses: actions/github-script@v7 + with: + script: | + const body = (context.payload.comment?.body || '').trim(); + const parts = body.split(/\s+/); + let cmd = (parts[1] || 'check').toLowerCase(); + const allowed = new Set(['help','check','licenses','sbom']); + if (!allowed.has(cmd)) cmd = 'help'; + core.setOutput('cmd', cmd); + core.setOutput('do_licenses', String(cmd === 'check' || cmd === 'licenses')); + core.setOutput('do_sbom', String(cmd === 'check' || cmd === 'sbom')); + + - name: Show help + if: ${{ steps.parse.outputs.cmd == 'help' }} + uses: actions/github-script@v7 + with: + script: | + const {owner, repo} = context.repo; + const help = [ + '### /gov help', + '', + 'Usage:', + '- `/gov` or `/gov check` — summarize both ScanCode and SBOM artifacts', + '- `/gov licenses` — summarize ScanCode (licenses) only', + '- `/gov sbom` — summarize SBOM (packages) only', + ].join('\n'); + await github.rest.issues.createComment({owner, repo, issue_number: context.issue.number, body: help}); + + - name: Extract PR info + if: ${{ steps.parse.outputs.cmd != 'help' }} + id: pr + uses: actions/github-script@v7 + with: + script: | + const {owner, repo} = context.repo; + const prNumber = context.issue.number; + const {data: pr} = await github.rest.pulls.get({owner, repo, pull_number: prNumber}); + core.setOutput('head_ref', pr.head.ref); + core.setOutput('head_sha', pr.head.sha); + core.setOutput('base_ref', pr.base.ref); + + - name: Find latest PR Governance run for this PR + if: ${{ steps.parse.outputs.cmd != 'help' }} + id: findrun + uses: actions/github-script@v7 + env: + HEAD_REF: ${{ steps.pr.outputs.head_ref }} + with: + script: | + const {owner, repo} = context.repo; + const headRef = process.env.HEAD_REF; + const {data} = await github.rest.actions.listWorkflowRunsForRepo({owner, repo, per_page: 50}); + const target = data.workflow_runs + .filter(r => r.head_branch === headRef && r.name === 'PR Governance (licenses & secrets)') + .sort((a,b) => new Date(b.created_at) - new Date(a.created_at))[0]; + if (!target) { + core.setFailed('No matching PR Governance run found for this branch.'); + return; + } + core.info(`Using run id: ${target.id}`); + core.setOutput('run_id', String(target.id)); + + - name: Download ScanCode artifact + if: ${{ steps.parse.outputs.cmd != 'help' }} + uses: dawidd6/action-download-artifact@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + run_id: ${{ steps.findrun.outputs.run_id }} + name: scancode-report + path: gov-artifacts + if_no_artifact_found: warn + + - name: Download SBOM artifact + if: ${{ steps.parse.outputs.cmd != 'help' }} + uses: dawidd6/action-download-artifact@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + run_id: ${{ steps.findrun.outputs.run_id }} + name: sbom-spdx + path: gov-artifacts + if_no_artifact_found: warn + + - name: List downloaded files (debug) + if: ${{ steps.parse.outputs.cmd != 'help' }} + run: | + echo "Downloaded artifacts:" + find gov-artifacts -maxdepth 3 -type f -print || true + + - name: Summarize reports + if: ${{ steps.parse.outputs.cmd != 'help' }} + id: summarize + shell: bash + run: | + set -euo pipefail + if ! command -v jq >/dev/null 2>&1; then + sudo apt-get update && sudo apt-get install -y jq + fi + mkdir -p gov-artifacts + summary=gov-artifacts/GOV_SUMMARY.md + BUF=$'' + append_line() { BUF+="$1"$'\n'; } + append_blank() { BUF+=$'\n'; } + + append_line "## Governance reports summary" + append_line "Run ID: ${{ steps.findrun.outputs.run_id }}" + append_blank + + COP=0 + UNK=0 + # Resolve file paths when artifact names or folders differ + SCAN_FILE="gov-artifacts/scancode.json" + if [ ! -f "$SCAN_FILE" ]; then + alt=$(find gov-artifacts -type f -name 'scancode*.json' | head -n1 || true) + if [ -n "${alt:-}" ]; then SCAN_FILE="$alt"; fi + fi + SBOM_FILE="gov-artifacts/sbom.spdx.json" + if [ ! -f "$SBOM_FILE" ]; then + alt=$(find gov-artifacts -type f \( -name '*.spdx.json' -o -name 'sbom*.json' \) | head -n1 || true) + if [ -n "${alt:-}" ]; then SBOM_FILE="$alt"; fi + fi + + if [ "${{ steps.parse.outputs.do_licenses }}" = "true" ]; then + if [ -f "$SCAN_FILE" ]; then + COP=$(jq -r '[.files[]? | .licenses[]? | (.spdx_license_key // "") | ascii_downcase | select(test("(^|[^a-z])(agpl|gpl|lgpl)([^a-z]|$)"))] | length' "$SCAN_FILE") + UNK=$(jq -r '[.files[]? | .licenses[]? | (.spdx_license_key // "") | ascii_downcase | select(. == "unknown" or . == "noassertion" or . == "")] | length' "$SCAN_FILE") + append_line "### ScanCode (licenses)" + append_line "- Copyleft findings (AGPL/GPL/LGPL): ${COP}" + append_line "- Unknown/NoAssertion licenses: ${UNK}" + append_line "- Top files with copyleft/unknown:" + TOP=$(jq -r '( + [.files[]? | select(.licenses) | {path: .path, keys: ([.licenses[]? | (.spdx_license_key // "")|ascii_downcase])}] + | map(select((.keys|join(" ")) | test("agpl|gpl|lgpl|unknown|noassertion"))) + | .[:5] + | map(" - " + .path) + | .[]) + ' "$SCAN_FILE" || true) + if [ -n "${TOP:-}" ]; then BUF+="$TOP"$'\n'; fi + append_blank + else + append_line "### ScanCode (licenses)" + append_line "- Artifact not found." + append_blank + fi + fi + + if [ "${{ steps.parse.outputs.do_sbom }}" = "true" ]; then + if [ -f "$SBOM_FILE" ]; then + TOTAL=$(jq -r '[.packages[]?] | length' "$SBOM_FILE") + GPL=$(jq -r '[.packages[]? | (.licenseConcluded // .licenseDeclared // "") | ascii_downcase | select(test("(^|[^a-z])(agpl|gpl|lgpl)([^a-z]|$)"))] | length' "$SBOM_FILE") + NOA=$(jq -r '[.packages[]? | (.licenseConcluded // .licenseDeclared // "") | ascii_downcase | select(. == "noassertion" or . == "unknown" or . == "")] | length' "$SBOM_FILE") + append_line "### SBOM (SPDX)" + append_line "- Packages: ${TOTAL}" + append_line "- Copyleft package licenses (AGPL/GPL/LGPL): ${GPL}" + append_line "- Unknown/NoAssertion package licenses: ${NOA}" + append_blank + else + append_line "### SBOM (SPDX)" + append_line "- Artifact not found." + append_blank + fi + fi + + printf "%s" "$BUF" > "$summary" + printf "%s\n" "copyleft_files=${COP}" >> "$GITHUB_OUTPUT" + printf "%s\n" "unknown_files=${UNK}" >> "$GITHUB_OUTPUT" + printf "%s" "$BUF" >> "$GITHUB_STEP_SUMMARY" + + - name: Comment summary on PR + if: ${{ steps.parse.outputs.cmd != 'help' }} + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const {owner, repo} = context.repo; + const body = fs.readFileSync('gov-artifacts/GOV_SUMMARY.md','utf8'); + await github.rest.issues.createComment({owner, repo, issue_number: context.issue.number, body}); + + - name: Add label if review needed + if: ${{ steps.parse.outputs.cmd != 'help' && (steps.summarize.outputs.copyleft_files != '0' || steps.summarize.outputs.unknown_files != '0') }} + uses: actions/github-script@v7 + with: + script: | + const {owner, repo} = context.repo; + const labels = ['license-review-needed']; + await github.rest.issues.addLabels({owner, repo, issue_number: context.issue.number, labels}); diff --git a/.github/workflows/governance-smoke.yml b/.github/workflows/governance-smoke.yml new file mode 100644 index 0000000..f149a76 --- /dev/null +++ b/.github/workflows/governance-smoke.yml @@ -0,0 +1,33 @@ +# @ai-generated: true +# @ai-tool: Copilot +name: Governance Smoke Tests + +on: + pull_request: + paths: + - '.github/workflows/**' + - 'tests/governance/**' + push: + paths: + - '.github/workflows/**' + - 'tests/governance/**' + +permissions: + contents: read + +jobs: + node-smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Run governance smoke tests (if present) + shell: bash + run: | + if [ -f tests/governance/smoke.test.js ]; then + node tests/governance/smoke.test.js + else + echo "No governance smoke test found; skipping" + fi diff --git a/.github/workflows/pr-autolinks.yml b/.github/workflows/pr-autolinks.yml new file mode 100644 index 0000000..86680a4 --- /dev/null +++ b/.github/workflows/pr-autolinks.yml @@ -0,0 +1,136 @@ +# @ai-generated: true +# @ai-tool: Copilot +name: PR Auto Links (/gov autofill) + +on: + issue_comment: + types: [created] + +permissions: + contents: read + pull-requests: write + actions: read + +jobs: + autolinks: + if: >- + ${{ github.event.issue.pull_request && + (startsWith(github.event.comment.body || '', '/gov autofill') || + startsWith(github.event.comment.body || '', '/gov links')) }} + runs-on: ubuntu-latest + steps: + - name: Preview or apply autofill + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const cmd = (context.payload.comment?.body || '').trim(); + const apply = /^\/gov\s+autofill\s+apply!?/i.test(cmd); + const previewOnly = /^\/gov\s+links/i.test(cmd) || /^\/gov\s+autofill\s*$/i.test(cmd); + + const {owner, repo} = context.repo; + const prNumber = context.issue.number; + const {data: pr} = await github.rest.pulls.get({owner, repo, pull_number: prNumber}); + + // Compute change flags and latest run links + const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number: prNumber, per_page: 100 }); + const changed = files.map(f => f.filename); + const rx = { + userUI: /(^|\/)(ui|web|frontend|public|templates)(\/|$)|(^|\/)src\/.*\.(html|tsx?|vue)$/i, + sensitive: /(^|\/)(auth|authn|authz|login|acl|permissions?|access[_-]?control|secrets?|tokens?|jwt|oauth)(\/|$)|\.(policy|rego)$/i, + infra: /(^|\/)(k8s|kubernetes|helm|charts|deploy|ops|infra|infrastructure|manifests|terraform|ansible)(\/|$)|(^|\/)dockerfile$|docker-compose\.ya?ml$|Chart\.ya?ml$/i, + backend: /(^|\/)(src|api|server|backend|app)(\/|\/.*)([^\/]+)\.(js|ts|py|rb|go|java|cs)$/i, + media: /\.(png|jpe?g|gif|webp|svg|mp4|mp3|wav|pdf)$/i, + data: /(^|\/)(data|datasets|training|notebooks|scripts)(\/|$)/i + }; + const has = (re) => changed.some(p => re.test(p)); + const flags = { + userUI: has(rx.userUI), + sensitive: has(rx.sensitive), + infra: has(rx.infra), + backend: has(rx.backend), + media: has(rx.media), + data: has(rx.data) + }; + + const prBody = (pr.body || '').toString(); + const {data} = await github.rest.actions.listWorkflowRunsForRepo({owner, repo, per_page: 100}); + const pick = (name) => data.workflow_runs + .filter(r => r.head_branch === pr.head.ref && r.name === name) + .sort((a,b) => new Date(b.created_at) - new Date(a.created_at))[0]; + const gov = pick('PR Governance (licenses & secrets)'); + const tests = pick('Run Unit Tests'); + const mkRunUrl = (r) => r ? `https://github.com/${owner}/${repo}/actions/runs/${r.id}` : ''; + const links = { + governance_run: mkRunUrl(gov), + scancode_report: mkRunUrl(gov), + sbom_report: mkRunUrl(gov), + unit_tests: mkRunUrl(tests) + }; + + if (previewOnly && !apply) { + const rows = []; + if (links.governance_run) { + rows.push(`- ScanCode report: ${links.governance_run} (artifact: scancode-report)`); + rows.push(`- SBOM (SPDX): ${links.sbom_report} (artifact: sbom-spdx)`); + } else { + rows.push('- ScanCode/SBOM: not found yet (run PR Governance workflow)'); + } + if (links.unit_tests) rows.push(`- Unit tests: ${links.unit_tests}`); + rows.push('- C2PA: N/A'); + rows.push('- Accessibility statement: N/A'); + rows.push('- Retention schedule: N/A'); + rows.push('- Log retention policy: N/A'); + rows.push('- Smoke test: use latest Unit tests run link above'); + + const body = [ + '### Auto-links suggestions', + '', + ...rows, + '', + 'Tip: Use \'/gov autofill apply\' to apply safe defaults (N/A where allowed) and add run links into the PR body.' + ].join('\n'); + + await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body }); + return; + } + + if (apply) { + let body = prBody; + const updateField = (text, label, value, opts={}) => { + const re = new RegExp(`(^|\n)([\t ]*)${label}[\t ]*:[\t ]*(.*)$`, 'i'); + const m = text.match(re); + if (!m) return text; // line not present + const current = m[3].trim(); + const isPlaceholder = current === '' || /^<.*>$/.test(current) || /^N\/?A$/i.test(current); + if (!isPlaceholder && !opts.force) return text; + const prefix = m[1] + (m[2] || ''); + return text.replace(re, `${prefix}${label}: ${value}`); + }; + + body = updateField(body, 'C2PA', 'N/A'); + body = updateField(body, 'Accessibility statement', 'N/A'); + body = updateField(body, 'Retention schedule', 'N/A'); + body = updateField(body, 'Log retention policy', 'N/A'); + if (links.unit_tests) body = updateField(body, 'Smoke test', links.unit_tests); + + if (!/##\s*Governance artifacts/i.test(body)) { + const extras = []; + extras.push('', '## Governance artifacts', ''); + if (links.governance_run) { + extras.push(`- ScanCode: ${links.governance_run} (artifact: scancode-report)`); + extras.push(`- SBOM: ${links.sbom_report} (artifact: sbom-spdx)`); + } + if (links.unit_tests) extras.push(`- Unit tests: ${links.unit_tests}`); + body += '\n' + extras.join('\n') + '\n'; + } + + await github.rest.pulls.update({ owner, repo, pull_number: prNumber, body }); + const msg = [ + 'Applied auto-fill updates to the PR body:', + '- Set N/A for C2PA, Accessibility statement, Retention schedule, Log retention policy (placeholders only).', + '- Filled Smoke test with latest Unit Tests run link (if available).', + '- Appended Governance artifacts section with run links.' + ].join('\n'); + await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body: msg }); + } diff --git a/.github/workflows/pr-governance.yml b/.github/workflows/pr-governance.yml new file mode 100644 index 0000000..36ea2f5 --- /dev/null +++ b/.github/workflows/pr-governance.yml @@ -0,0 +1,38 @@ +# @ai-generated: true +# @ai-tool: Copilot +name: PR Governance (licenses & secrets) + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + actions: read + contents: read + pull-requests: write + issues: write + security-events: write + +jobs: + governance: + name: Reusable AI governance checks + # NOTE for downstream projects: + # This reference works only when calling the reusable workflow from THIS repository. + # After you copy a local version of `.github/workflows/ai-governance.yml` into your project, + # update the 'uses:' line to your repo path, e.g.: + # uses: //.github/workflows/ai-governance.yml@main + # Otherwise the workflow_call will fail in the consumer repository. + uses: ./.github/workflows/ai-governance.yml + with: + run_markdownlint: true + run_scancode: true + run_sbom: true + run_gitleaks: false + run_dependency_review: false + run_codeql: false + lint_command: 'make fmt' + test_command: 'make test' + require_ui_transparency: true + require_dpia_for_user_facing: true + require_eval_for_high_risk: false + enable_post_merge_reminders: true diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml new file mode 100644 index 0000000..b017323 --- /dev/null +++ b/.github/workflows/run-unit-tests.yml @@ -0,0 +1,68 @@ +# @ai-generated: true +# @ai-tool: Copilot +name: Run Unit Tests + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + +jobs: + node_tests: + name: Node.js tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Install dependencies (if package.json exists) + shell: bash + run: | + if [ -f package.json ]; then + if [ -f package-lock.json ]; then npm ci; else npm install; fi + else + echo "No package.json; skipping Node setup"; exit 0 + fi + - name: Run npm test (if defined) + shell: bash + run: | + if [ -f package.json ]; then + if node -e "const p=require('./package.json');process.exit(p.scripts&&p.scripts.test?0:1)"; then + npm test --silent || (echo "::error::npm test failed"; exit 1) + else + echo "No test script defined; skipping Node tests"; + fi + else + echo "No package.json; skipping"; + fi + + python_tests: + name: Python tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install dependencies + shell: bash + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then + pip install -r requirements.txt || true + fi + # Ensure pytest available + pip install pytest || true + - name: Run pytest (if tests exist) + shell: bash + env: + PYTHONPATH: ${{ github.workspace }} + run: | + if find tests -type f -name "*.py" 2>/dev/null | grep -q .; then + pytest -q || (echo "::error::pytest failed"; exit 1) + else + echo "No Python tests found; skipping" + fi diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml new file mode 100644 index 0000000..8c7155e --- /dev/null +++ b/.github/workflows/workflow-lint.yml @@ -0,0 +1,28 @@ +# @ai-generated: true +# @ai-tool: Copilot +name: Workflow Lint (actionlint) + +on: + pull_request: + paths: + - '.github/workflows/**' + push: + paths: + - '.github/workflows/**' + +permissions: + contents: read +jobs: + actionlint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install shellcheck + run: sudo apt-get update && sudo apt-get install -y shellcheck + - name: Install actionlint (pinned) + shell: bash + run: | + curl -sSfL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash | bash -s 1.7.1 + - name: Run actionlint + shell: bash + run: ./actionlint -shellcheck=shellcheck diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..4ab9abb --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,16 @@ +{ + "default": true, + "MD013": false, + "MD034": false, + "MD033": false, + "MD029": false, + "MD007": false, + "MD012": false, + "MD009": false, + "MD010": false, + "MD040": false, + "MD025": false, + "MD022": { "lines_above": 0, "lines_below": 0 }, + "MD031": false, + "MD032": false +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..68f3959 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,23 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Prepare PR body file", + "type": "shell", + "command": "bash -lc 'tmp=$(mktemp); cp .github/pull_request_template.md \"$tmp\"; sed -i \"s##Bootstrap governance workflows and PR template; normalize uses to local.#\" \"$tmp\"; sed -i \"s##GitHub Copilot gpt-5#\" \"$tmp\"; sed -i \"s##2025-09-16T10:00:00Z#\" \"$tmp\"; sed -i \"s##@ErykKul#\" \"$tmp\"; sed -i \"s#provider|deployer#deployer#\" \"$tmp\"; sed -i \"s#\\- \\[ \\] No secrets/PII#- [x] No secrets/PII#I\" \"$tmp\"; sed -i \"s#\\- \\[ \\] Agent logging enabled#- [x] Agent logging enabled#I\" \"$tmp\"; sed -i \"s#\\- \\[ \\] Kill-switch / feature flag present#- [x] Kill-switch / feature flag present#I\" \"$tmp\"; sed -i \"s#Risk classification: limited|high#Risk classification: limited#\" \"$tmp\"; sed -i \"s#Personal data: yes|no#Personal data: no#\" \"$tmp\"; sed -i \"s#DPIA: #DPIA: N/A#\" \"$tmp\"; sed -i \"s#Automated decision-making: yes|no#Automated decision-making: no#\" \"$tmp\"; sed -i \"s#Agent mode used: yes|no#Agent mode used: yes#\" \"$tmp\"; sed -i \"s#\\- \\[ \\] No prohibited practices under EU AI Act#- [x] No prohibited practices under EU AI Act#I\" \"$tmp\"; sed -i \"s#\\- \\[ \\] License/IP attestation#- [x] License/IP attestation#I\" \"$tmp\"; sed -i \"s#Attribution: #Attribution: N/A#\" \"$tmp\"; sed -i \"s#GPAI obligations: #GPAI obligations: N/A#\" \"$tmp\"; sed -i \"s#Vendor GPAI compliance reviewed: #Vendor GPAI compliance reviewed: N/A#\" \"$tmp\"; echo \"$tmp\"'", + "problemMatcher": [ + "$eslint-stylish" + ], + "group": "build" + }, + { + "label": "Prepare PR body file (safe sed)", + "type": "shell", + "command": "bash -lc 'tmp=$(mktemp); cp .github/pull_request_template.md \"$tmp\"; perl -0777 -pe \"s//Bootstrap governance workflows and PR template; normalize uses to local./g; s//GitHub Copilot gpt-5/g; s//2025-09-16T10:00:00Z/g; s//@ErykKul/g; s/provider\\|deployer/deployer/g; s/- \\[ \\] No secrets\\/PII/- [x] No secrets\\/PII/ig; s/- \\[ \\] Agent logging enabled/- [x] Agent logging enabled/ig; s/- \\[ \\] Kill-switch \\/ feature flag present/- [x] Kill-switch \\/ feature flag present/ig; s/Risk classification: limited\\|high/Risk classification: limited/g; s/Personal data: yes\\|no/Personal data: no/g; s/DPIA: /DPIA: N\\/A/g; s/Automated decision-making: yes\\|no/Automated decision-making: no/g; s/Agent mode used: yes\\|no/Agent mode used: yes/g; s/- \\[ \\] No prohibited practices under EU AI Act/- [x] No prohibited practices under EU AI Act/ig; s/- \\[ \\] License\\/IP attestation/- [x] License\\/IP attestation/ig; s/Attribution: /Attribution: N\\/A/g; s/GPAI obligations: /GPAI obligations: N\\/A/g; s/Vendor GPAI compliance reviewed: /Vendor GPAI compliance reviewed: N\\/A/g;\" -i \"$tmp\"; echo \"$tmp\"'", + "problemMatcher": [ + "$eslint-stylish" + ], + "group": "build" + } + ] +} \ No newline at end of file diff --git a/ai-context.md b/ai-context.md new file mode 100644 index 0000000..761bbc2 --- /dev/null +++ b/ai-context.md @@ -0,0 +1,461 @@ + + +# AI Context — Reusable Governance (Pilot/Production) + +Purpose + +- Provide a minimal, reusable governance context for any repo. +- Bootstrap with org-standard .github workflows and PR template from this project: https://github.com/libis/ai-transition +- Keep itself and workflows up to date using timestamps and a simple verify step. +- Capture provenance and propose context updates only after the user reads this context. + +Scope + +- Safe to use in pilots and production. +- No repo-local CI logic beyond a small consumer workflow that reuses org workflows. +- Excludes Annex III high-risk domains unless explicitly escalated. + +## Commands the agent must support + +Important: Act only after the user invokes these commands. Before making changes, enumerate planned edits and ask for confirmation. + +2. verify + +- Goal: Check if this context and .github are up to date. +- Steps: + - Compare this file’s @version/@last_context_update to upstream at @template_source. + - For .github, compare local files to upstream names and checksums using the Content API `sha` fields. + - Summarize: + + - Context: up-to-date | behind (show upstream version) + - .github: in sync | drifted | unknown (show planned changes) + + - If drifted, offer “update_workflows”. + + - After running, update `last_verified_at` (UTC ISO-8601) in the State block. + +3. update_workflows + +- Goal: Sync .github from the upstream project, preserving local customizations. +- Steps: + - Show planned adds/modifies/removes for .github. + - For modified files, show a 3-way diff proposal. + - After confirmation, apply changes, update .github/ai-transition-sync.json and @last_github_sync. + - Open PR: "chore(ai): sync governance workflows". + + - Use the open_pr command described below to standardize PR creation via GitHub CLI. + +4. log_provenance + +- Goal: Add AI-Assistance provenance to the PR body. +- Insert if missing: + - AI-Assistance Provenance + + - Prompt: + - Model: + - Date: + - Author: @ + - Reviewer confirms: [ ] No secrets/PII; [ ] Licensing respected + - Notes: + +5. open_pr + +- Goal: Create a PR by committing to a new branch and invoking the GitHub CLI using the repo’s PR template at .github/pull_request_template.md. +- Inputs (recommended): + - title (string), default: context-specific e.g., "chore(ai): bootstrap governance workflows and PR template" + - branch (string), default: auto-generate e.g., "ai/bootstrap-" + - base (string), default: detect repo default branch (fallback: main) + - labels (array), default: ["ai", "governance"] + - draft (bool), default: true + - reviewers (array of handles), optional + - body_append (string), optional extra notes (e.g., provenance) +- Steps: + 1. Verify prerequisites: + - Ensure Git is initialized and remote origin exists. + - Ensure GitHub CLI is installed and authenticated: `gh auth status`. + 2. Branch & commit: + - Create/switch: `git checkout -b `. + - Stage: `git add -A`. + - Commit: `git commit -m ""` (add a second -m with a short summary if useful). + - Push: `git push -u origin <branch>`. + 3. Prepare body: + + - Always build the PR body from `.github/pull_request_template.md` and FILL IT IN-PLACE. Do not append a second provenance block. + - Steps: + - Copy the template to a temp file. + - Replace placeholders with real values (or N/A where allowed) directly in the existing sections: + - AI Provenance: Prompt, Model, Date (UTC ISO-8601 Z), Author, Role (provider|deployer) + - Compliance checklist: set required checkboxes, risk classification, personal data, ADM, agent mode, vendor GPAI (if deployer), attribution + - Change-type specifics: add only relevant lines; remove optional placeholders that don’t apply + - Tests & Risk: add rollback plan, smoke link if high-risk + - Ensure only one `- Date:` line exists and it contains a real timestamp; remove inline hints if needed. + - Pre-flight validation (local): + - Confirm these patterns exist in the body: + - Prompt:, Model:, Date: 20..-..-..T..:..:..Z, Author: + - Role: (provider|deployer) + - [x] No secrets/PII, [x] Agent logging enabled, [x] Kill-switch / feature flag present, [x] No prohibited practices + - Risk classification: (limited|high), Personal data: (yes|no), Automated decision-making: (yes|no), Agent mode used: (yes|no) + - If Role=deployer → Vendor GPAI compliance reviewed: (https://…|N/A) + - No `<…>` or `${…}` placeholders remain. + - Optionally append `body_append` at the end for extra notes (avoid duplicating provenance). + + 4. Create PR: + - Detect base branch (prefer repo default); fallback to `main`. + - Run: `gh pr create -B <base> -H <branch> --title "<title>" --body-file <temp_body_path> --draft` + - Add labels inline: `--label ai --label governance` (plus any provided). + - Add reviewers if provided: `--reviewer user1 --reviewer user2`. + 5. Output PR URL and short summary of changes. + + Notes: + + - Language detection heuristic: use `git ls-files` to check for common extensions (e.g., `*.py`, `*.js`, `*.ts`, `*.go`, `*.java`) and toggle inputs accordingly. + + - When you introduce new language toggles locally, propose them upstream (same repo) so future pilots get them by default. + + - Labels: ensure default labels exist or create them if you have permissions; otherwise proceed without labels. + +6. record_update + +- Goal: Update header timestamps when this context or .github sync changes. +- Update @last_context_update after content changes. +- Update @last_github_sync after workflow syncs. Keep ISO-8601. + +7. suggest_context_note + +- Goal: While working, when relevant information emerges that would help future work, propose a small addition to this context. +- Constraints: Only suggest after the user asks to "read context". Keep notes concise and reusable. + +8. toggle_suggestions + +- Goal: Respect per-user opt-out for suggestions. +- Mechanism: + + - Local file: .ai/context.local.json (create/update). + - Example: + + ```json + + { + "suggestions_enabled": false, + "user": { "name": "<name>", "email": "<email>" } + } + + ``` + + - When false, do not surface proactive suggestions; act only on explicit commands. + +## What lives in .github (discover dynamically) + +Always enumerate live contents from upstream first. As of this template’s creation, the upstream project contains: + +- CODEOWNERS +- pull_request_template.md +- workflows/ai-governance.yml (reusable governance) +- workflows/ai-agent.yml (ChatOps helpers) +- workflows/code-review-agent.yml (code review agent) +- workflows/copilot-pr-review.yml (on-demand AI review) +- workflows/gov-review.yml (governance artifacts reviewer) +- workflows/governance-smoke.yml (smoke checks) +- workflows/pr-autolinks.yml (auto links/NAs) +- workflows/pr-governance.yml (PR governance helpers) +- workflows/run-unit-tests.yml (unit test runner) +- workflows/workflow-lint.yml (lint GitHub workflows) + +If any expected file is absent upstream when bootstrapping, warn and proceed with available items only. + +## Runtime profile — VS Code GitHub Copilot Agent (gpt-5) + +- Environment: VS Code with GitHub Copilot Agents; model target: gpt-5. +- Style: short, skimmable outputs; minimal Markdown. +- Action cadence: only after “read context”; list intended edits first; checkpoint after a few actions or >3 file edits. +- Smallest viable change: preserve style; avoid broad refactors; split risky work. +- Terminal usage: run git/gh and quick checks in the integrated terminal; never expose secrets. +- PR hygiene: always include Provenance (Prompt/Model/Date/Author); labels ai/governance; default to draft PRs. +- Quality gates: when code/workflows change, run a fast lint/test and report PASS/FAIL succinctly. +- Suggestions policy: suggest updates only after “read context”; users can opt out via `.ai/context.local.json`. + +### Context recall protocol + +- Re-read before you write: before each multi-step action or tool batch, re-open this `ai-context.md` and re-read the exact sections that govern the task (typically: “Developer prompts”, “Runtime profile”, “PR body generation rules”, and “Baseline controls”), plus any referenced docs under `governance/`. +- Don’t trust memory for strict fields: when populating provenance/risk sections or workflow inputs, copy the exact scaffold and rules from this file instead of paraphrasing. +- Keep an active checklist: derive a short requirements checklist from the user’s ask and keep it visible; verify each item before ending a turn. +- Maintain a scratchpad of snippets: keep a small, ephemeral list of the exact lines you’re following (with heading names). Refresh it after every 3–5 tool calls or after editing more than ~3 files. +- Periodic refresh: if the session runs long (>10 minutes) or after significant context edits, quickly re-scan this file (search for “PR body generation rules”, “Baseline controls”, “MUST|required|ISO-8601”) to avoid missing details. +- Resolve drift immediately: if you change this context in the same PR, re-read the modified sections and reconcile instructions before continuing. +- Token discipline: when constrained, fetch only the specific snippets you need (by heading) rather than relying on summaries. + +## How to: bootstrap, verify, iterate + +- Bootstrap adds: + - Reusable workflows under `.github/workflows/` (including `ai-governance.yml`, `pr-governance.yml`, `governance-smoke.yml`, `run-unit-tests.yml`). + - PR template `.github/pull_request_template.md` aligned with governance checks. + - A minimal `tests/governance/smoke.test.js` so smoke passes out-of-the-box. +- After opening a PR, comment `/gov` for a governance summary and Copilot review. Use `/gov links` to preview artifact links and `/gov autofill apply` to fill placeholders. +- If your repo lacks lint/tests, set empty `lint_command`/`test_command` in the consumer `pr-governance.yml` or disable those inputs. +- AI headers: ensure YAML workflows start with + - `# @ai-generated: true` and `# @ai-tool: Copilot` at the very top. + +### Notes from comparing ".github copy" vs current (for downstream consumers) + +- pr-governance.yml uses a repository path in the `uses:` field. After you copy workflows into your repo, update it to your repo path or switch it to a local reference. Example: + - uses: <OWNER>/<REPO>/.github/workflows/ai-governance.yml@main + - or when the workflow is local: uses: ./.github/workflows/ai-governance.yml +- ScanCode job is simplified to pip install of scancode-toolkit and always uploads `scancode.json` as `scancode-report`. Ensure artifacts exist for `/gov` commands to work. +- Unit tests runner is language-aware and skips when files aren’t present. If your project has nested apps (e.g., UI/backend folders), adapt the install-and-test blocks accordingly. +- Governance smoke workflow skips when `tests/governance/smoke.test.js` is absent. Keep or remove this file based on your project layout. +- ChatOps commands (`/gov`, `/gov links`, `/gov autofill apply`, `/gov copilot`) assume the workflow names: + - "PR Governance (licenses & secrets)" and "Run Unit Tests". If you rename workflows, update the matching logic in `pr-autolinks.yml` and `gov-review.yml`. +- CODEOWNERS in the template points to placeholders (e.g., @ErykKul). Replace with your org teams (e.g., @libis/security) to enforce reviews on governance and policy paths. +- Lint/Test inputs: the consumer `pr-governance.yml` accepts `lint_command` and `test_command`. Set them to project-specific commands (e.g., `make fmt && make check` and `make test`), or set to empty strings `''` to skip when your repo has no lint/tests yet. + +## PR body generation rules (for Copilot/Agents) + +Do not fabricate links or closing keywords: + +- Only include “Closes #<num>”, “Fixes #<num>”, etc., when a real issue exists. +- Never add “Closes N/A” or similar placeholders. +- If there’s no issue, omit the closing keyword entirely. + +Global body constraints (must follow for this repo): + +- No backticks anywhere in the PR body. Do not format text with inline/backtick code blocks. Prefer plain text or single quotes if needed. +- No unresolved placeholders: do not leave `<...>`, `${...}`, `$(...)`, or similar tokens anywhere in the body. +- When referencing code identifiers, write them as plain text without backticks. +- If you copy a template snippet, scrub it to remove any of the above before posting. + +Summary (must-fill): + +- Provide 1–3 bullets summarizing what changed and why. +- Link related issues when applicable. +- Do not insert a placeholder bullet if there’s nothing substantive to add; keep the section concise. + +Golden scaffold (fill exactly; replace all <…> with a concrete value or N/A): + +- Prompt: <paste exact prompt or link to prompt file/snippet> +- Model: <e.g., GitHub Copilot gpt-5> +- Date: <UTC ISO-8601, e.g., 2025-09-12T14:23:45Z> +- Author: @<github-handle> // Do NOT include names or emails +- Role: provider|deployer (choose one) +- Vendor GPAI compliance reviewed: <https://… or N/A> (required if Role=deployer) +- [ ] No secrets/PII +- Risk classification: limited|high +- Personal data: yes|no +- DPIA: <https://… or N/A> +- Automated decision-making: yes|no +- Agent mode used: yes|no +- [ ] Agent logging enabled +- [ ] Kill-switch / feature flag present +- [ ] No prohibited practices under EU AI Act +- Attribution: <https://… or N/A> + +Strict fill rules for the model: + +- Author field MUST be a GitHub handle only (e.g., @octocat). Names and emails are forbidden. +- No backticks anywhere in the PR body (repo policy; enforced by CI/local checker). +- No unresolved placeholders anywhere in the PR body: remove/replace `<…>`, `${…}`, `$(…)`. + +Strict fill rules for the model: + +- Author field MUST be a GitHub handle only (e.g., @octocat). Names and emails are forbidden. + +Self-check before posting (the agent should verify these patterns): + +- Date matches: ^20\d{2}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$ and does not contain ${ or <>. +- Contains: Prompt:, Model:, Date:, Author:. +- Contains: Role: (provider|deployer) exactly one. +- Contains: Risk classification: (limited|high). +- Contains: Personal data: (yes|no), Automated decision-making: (yes|no), Agent mode used: (yes|no). +- Contains checkbox: [x] No secrets/PII (case-insensitive). +- If UI changed, includes transparency and accessibility lines; if media changed, includes AI content labeled + C2PA; if backend changed, includes ASVS. +- If risk=high or ADM=yes, includes Oversight plan link, Rollback plan, Smoke test link. + +Local checker hard constraints (what a local script would enforce): + +- Script (optional): `scripts/local_provenance_check.sh <owner/repo> <pr-number>` pulls the current PR body and validates it. +- It fails if the body contains any backticks, `${...}`, `$(...)`, or `<...>` placeholders anywhere. +- Required checkboxes/fields include: `[x] No secrets/PII`, `[x] Agent logging enabled`, `[x] Kill-switch / feature flag present`, Role, Risk classification, Personal data, Automated decision-making, Agent mode used. +- When Agent mode is yes or Risk is high, it also requires `[x] Human oversight retained`. +- Before creating/editing a PR, scrub the body to remove disallowed tokens and ensure all required lines are present. + +Two minimal examples + +Note: Examples must avoid PII; use `@<github-handle>` only for attribution. + +- Limited, non-UI backend change + - Prompt: “Refactor YAML linter invocation to use pinned version; add smoke test.” + - Model: GitHub Copilot gpt-5 + - Date: 2025-09-12T10:21:36Z + - Author: @github-handle + - Role: deployer + - [x] No secrets/PII + - Risk classification: limited + - Personal data: no + - DPIA: N/A + - Automated decision-making: no + - Agent mode used: yes + - [x] Agent logging enabled + - [x] Kill-switch / feature flag present + - [x] No prohibited practices under EU AI Act + - Backend/API changed: + + - ASVS: N/A + + - Attribution: N/A + +- High risk, UI changed, ADM yes + - Prompt: “Add user-facing agent output; update transparency; link oversight and smoke.” + - Model: GitHub Copilot gpt-5 + - Date: 2025-09-12T10:45:03Z + - Author: @github-handle + - Role: deployer + - [x] No secrets/PII + - Risk classification: high + - Personal data: yes + - DPIA: https://example.org/dpia/ai-feature + - Automated decision-making: yes + - Agent mode used: yes + - [x] Agent logging enabled + - [x] Kill-switch / feature flag present + - [x] No prohibited practices under EU AI Act + - UI changed: + + - [x] Transparency notice updated + - Accessibility statement: https://example.org/accessibility + + - Media assets changed: + + - [x] AI content labeled + - C2PA: N/A + + - Backend/API changed: + + - ASVS: https://example.org/asvs/review + + - Oversight plan: https://example.org/oversight/plan + - Rollback plan: Feature flag off; revert PR. + - Smoke test: https://github.com/libis/your-repo/actions/runs/123456789 + - Attribution: N/A + +## Baseline controls to carry into all repos + +- Provenance in every PR: prompt/model/date/author (@handle only) + reviewer checks (no secrets/PII, licensing OK). +- No PII policy: do not include personal names, email addresses, phone numbers, or other identifiers in PR bodies, commit messages, or logs. Use @<github-handle> only where author attribution is required. +- License/IP hygiene: ScanCode in CI blocks AGPL/GPL/LGPL; use dependency review; avoid unapproved code pastes. +- Transparency (EU AI Act Art. 50): label AI-generated summaries; include disclosure text for user-facing outputs. +- Avoid prohibited practices (Art. 5): no emotion inference in workplace/education, no social scoring, no manipulative techniques, no biometric categorization. +- Annex III guardrails: exclude high-risk domains unless escalated. +- DPIA readiness: for user-facing agents; no PII in prompts/repos. +- Monitoring + rollback: SLIs (success %, defect %, unsafe block %, p95 latency) and feature-flag rollback. +- Pause rule: if validated error rate > 2% or any license/privacy/safety incident, pause and root-cause. + +## Agent coding guidelines (enforced by this context) + +- Prefer the smallest viable change + + - Keep diffs minimal; preserve existing style and public APIs. + - Reuse existing utilities; avoid duplication and broad refactors. + - Defer opportunistic cleanups to a separate PR. + +- Commit and PR discipline + + - Small, focused commits; one concern per commit. + - Commit message: `type(scope): summary` with a brief rationale and risk notes. + - Aim for compact PRs (< ~300 changed LOC when possible). Split larger ones. + +- Safety and verification first + + - Run quick quality gates on every substantive change: Build, Lint/Typecheck, Unit tests; report PASS/FAIL in the PR. + - Add or update minimal tests for new behavior (happy path + 1 edge case). + - Use feature flags for risky paths; ensure clear rollback. + +- Dependencies policy + + - Prefer stdlib and existing deps. Add new deps only with clear value. + - Pin versions and update manifests/lockfiles. Check license compatibility (no AGPL/GPL/LGPL where blocked). + +- Config and workflows + + - Reuse org workflows; don’t add bespoke CI beyond the minimal consumer workflow. + - Keep workflow permissions least-privilege. + +- Documentation and provenance + + - Update README or inline docs when behavior or interfaces change. + - Use `log_provenance` to append AI-Assistance details to the PR body. + +- Security, privacy, and IP + + - Never include secrets/PII; scrub logs; avoid leaking tokens. + - Respect copyright and licensing; cite sources where applicable. + +- Handling ambiguity + + - If under-specified, state 1–2 explicit assumptions and proceed; invite correction. + - If blocked by constraints, propose a minimal alternative and stop for confirmation. + +- Non-functional checks + + - Keep accessibility in mind for user-facing outputs. + - Note performance characteristics; avoid clear regressions; document complexity changes. + +- PR automation + + - Use `open_pr` to branch, commit, push, and create a draft PR via GitHub CLI with labels and reviewers. + +- Suggestions policy + - Only suggest context updates after the user invokes “read context”; honor user opt-out via `.ai/context.local.json`. + +## Developer prompts (after “read context”) + +- bootstrap → inspect upstream .github, propose copy + minimal consumer workflow if missing, then PR. +- verify → report context/.github drift; propose update_workflows if needed. +- update_workflows → sync .github with diffs and PR. +- log_provenance → add the provenance block to the PR body if missing. +- open_pr → branch, commit, push, and create a PR via GitHub CLI using the repo template. +- record_update → refresh timestamps in header. +- suggest_context_note → propose adding a concise, reusable note. +- toggle_suggestions off → write .ai/context.local.json to disable suggestions. + +## Source references (for reuse) + +- Project (source of truth): https://github.com/libis/ai-transition +- Pilot starter README (consumer workflow example): https://github.com/libis/ai-transition/blob/main/templates/pilot-starter/README.md +- Governance checks: https://github.com/libis/ai-transition/blob/main/governance/ci_checklist.md +- Risk mitigation matrix: https://github.com/libis/ai-transition/blob/main/governance/risk_mitigation_matrix.md +- EU AI Act notes: https://github.com/libis/ai-transition/blob/main/EU_AI_Act_gh_copilot.md +- Agent deployment controls: https://github.com/libis/ai-transition/blob/main/ai_agents_deployment.md +- Compliance table: https://github.com/libis/ai-transition/blob/main/LIBIS_AI_Agent_Compliance_Table.md + +## State (maintained by agent) + +```json + +{ + "template_source": "https://github.com/libis/ai-transition/blob/main/templates/pilot-starter/ai-context.md", + "template_version": "0.6.0", + "last_context_update": "2025-09-16T00:00:00Z", + "last_github_sync": "2025-09-16T00:00:00Z", + "last_verified_at": "2025-09-16T00:00:00Z", + "upstream_ref": "main", + "upstream_commit": "unknown" +} + +``` + +Notes for maintainers + +- Prefer pinning reusable workflows by tag or commit SHA instead of @main for regulated repos. +- Keep this file concise and org-agnostic; link deep policy detail from the org repo. +- If suggestions are noisy, default user-local suggestions to false via .ai/context.local.json.