@metasession.co/devaudit-cli 0.3.21 → 0.3.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metasession.co/devaudit-cli",
3
- "version": "0.3.21",
3
+ "version": "0.3.23",
4
4
  "description": "DevAudit CLI — installs, syncs, and operates the Metasession SDLC across consumer projects.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@clack/prompts": "^0.8.2",
36
- "@metasession.co/devaudit-plugin-sdk": "^0.3.21",
36
+ "@metasession.co/devaudit-plugin-sdk": "^0.3.23",
37
37
  "ajv": "^8.20.0",
38
38
  "commander": "^12.1.0",
39
39
  "consola": "^3.2.3",
@@ -161,18 +161,38 @@ jobs:
161
161
  run: |
162
162
  # stderr → /dev/null so warnings can't corrupt the JSON (DevAudit #48)
163
163
  npm audit --json > dependency-audit.json 2>/dev/null || true
164
- ACCEPTED="{{ACCEPTED_DEP_RISKS}}"
165
- UNACCEPTED=$(python3 -c "
166
- import json
164
+ # Optional, repository-owned exceptions must be advisory- and
165
+ # range-specific, approved, and unexpired. Missing means none.
166
+ UNACCEPTED=$(python3 - <<'PY' 2>/dev/null || echo "unknown"
167
+ import json, os
168
+ from datetime import date
167
169
  with open('dependency-audit.json') as f:
168
- data = json.load(f)
169
- accepted = set('${ACCEPTED}'.split()) if '${ACCEPTED}' else set()
170
- vulns = data.get('vulnerabilities', {})
171
- issues = [name for name, v in vulns.items()
172
- if v.get('severity') in ('high', 'critical')
173
- and name not in accepted]
174
- print(len(issues))
175
- " 2>/dev/null || echo "unknown")
170
+ audit = json.load(f)
171
+ path = 'compliance/security/accepted-vulnerabilities.json'
172
+ exceptions = []
173
+ if os.path.exists(path):
174
+ with open(path) as f:
175
+ exceptions = json.load(f).get('exceptions', [])
176
+ def accepted(package, finding):
177
+ advisory_ids = {
178
+ item.get('url', '').rsplit('/', 1)[-1]
179
+ for item in finding.get('via', []) if isinstance(item, dict)
180
+ }
181
+ for item in exceptions:
182
+ required = ('advisoryId', 'package', 'vulnerableRange', 'expiresAt', 'approvedBy', 'reason', 'remediationIssue')
183
+ if not all(item.get(key) for key in required):
184
+ continue
185
+ if item['expiresAt'] < date.today().isoformat():
186
+ continue
187
+ if item['package'] == package and item['vulnerableRange'] == finding.get('range') and item['advisoryId'] in advisory_ids:
188
+ print(f"Accepted temporary risk: {item['advisoryId']} ({package}) until {item['expiresAt']}")
189
+ return True
190
+ return False
191
+ findings = audit.get('vulnerabilities', {})
192
+ unresolved = [name for name, finding in findings.items()
193
+ if finding.get('severity') in ('high', 'critical') and not accepted(name, finding)]
194
+ print(len(unresolved))
195
+ PY
176
196
  echo "Unaccepted high/critical: $UNACCEPTED"
177
197
  if [ "$UNACCEPTED" != "0" ] && [ "$UNACCEPTED" != "unknown" ]; then
178
198
  echo "::error::$UNACCEPTED unaccepted high/critical vulnerability(ies)."
@@ -472,11 +492,11 @@ jobs:
472
492
  name: Upload Evidence
473
493
  runs-on: {{RUNNER}}
474
494
  needs: [quality-gates, register-release]
475
- # `always()` instead of `!failure()` so failed gates still upload their
476
- # evidence `status=failed` is itself the audit trail. `!cancelled()`
477
- # still guards against partial state on operator-cancel.
478
- # DevAudit-Installer#96.
479
- if: ${{ always() && github.ref_name == '{{INTEGRATION_BRANCH}}' && needs.register-release.result == 'success' }}
495
+ # `!cancelled()` preserves failed-gate evidence on eligible integration
496
+ # pushes, while a pull request remains an intentionally inapplicable,
497
+ # cleanly skipped job. `always()` caused GitHub to mark this job cancelled
498
+ # when Register Release was correctly skipped on a no-REQ PR (#500).
499
+ if: ${{ !cancelled() && github.event_name != 'pull_request' && github.ref_name == '{{INTEGRATION_BRANCH}}' && needs.register-release.result == 'success' }}
480
500
  env:
481
501
  DEVAUDIT_BASE_URL_VAR: ${{ vars.DEVAUDIT_BASE_URL }}
482
502
  DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
@@ -96,6 +96,29 @@ jobs:
96
96
  echo "Nothing to close out (idempotent no-op) — no PR opened."
97
97
  fi
98
98
 
99
+ - name: Report verified manual close-out catch-up
100
+ if: github.event_name == 'workflow_dispatch' && steps.closeout.outputs.changed != 'true' && steps.merge.outputs.merged != 'true'
101
+ run: |
102
+ set -euo pipefail
103
+ REQ="${{ steps.in.outputs.req }}"
104
+ PR_REF="${{ steps.in.outputs.pr }}"
105
+ [ -n "$REQ" ] && [ -n "$PR_REF" ] || { echo "::error::Manual no-op reporting requires release and release_pr."; exit 1; }
106
+ PR_JSON="$(gh pr view "$PR_REF" --repo "$GITHUB_REPOSITORY" --json mergedAt,baseRefName,headRefName,mergeCommit,number,url)"
107
+ HEAD="$(jq -r '.headRefName // empty' <<<"$PR_JSON")"
108
+ BASE="$(jq -r '.baseRefName // empty' <<<"$PR_JSON")"
109
+ MERGED="$(jq -r '.mergedAt // empty' <<<"$PR_JSON")"
110
+ SHA="$(jq -r '.mergeCommit.oid // empty' <<<"$PR_JSON")"
111
+ NUMBER="$(jq -r '.number' <<<"$PR_JSON")"
112
+ URL="$(jq -r '.url' <<<"$PR_JSON")"
113
+ [ -n "$MERGED" ] && [ "$BASE" = develop ] && [ "$HEAD" = "chore/close-out-$REQ" ] && [ -n "$SHA" ] || { echo "::error::release_pr must be a merged develop close-out PR for $REQ."; exit 1; }
114
+ BASE_URL="${DEVAUDIT_BASE_URL:-}"
115
+ [ -n "$BASE_URL" ] && [ -n "$DEVAUDIT_API_KEY" ] || { echo "::error::DevAudit base URL and project API key are required."; exit 1; }
116
+ RELEASE="$(curl --fail-with-body -sS -G "${BASE_URL%/}/api/ci/releases/resolve" -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" --data-urlencode "projectSlug={{PROJECT_SLUG}}" --data-urlencode "versionPrefix=${REQ}")"
117
+ RELEASE_ID="$(jq -r '.latest.id // empty' <<<"$RELEASE")"
118
+ RELEASE_VERSION="$(jq -r '.latest.version // empty' <<<"$RELEASE")"
119
+ [ "$RELEASE_VERSION" = "$REQ" ] && [ -n "$RELEASE_ID" ] || { echo "::error::Unable to resolve $REQ."; exit 1; }
120
+ curl --fail-with-body -sS -X POST "${BASE_URL%/}/api/ci/releases/${RELEASE_ID}/close-out" -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" -H 'Content-Type: application/json' --data "$(jq -nc --arg workflowRunId '${{ github.run_id }}' --arg workflowUrl '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' --arg pullRequestUrl "$URL" --arg mergeSha "$SHA" --argjson pullRequestNumber "$NUMBER" '{status:"completed",workflowRunId:$workflowRunId,workflowUrl:$workflowUrl,pullRequestNumber:$pullRequestNumber,pullRequestUrl:$pullRequestUrl,mergeSha:$mergeSha}')"
121
+
99
122
  - name: Close existing sync PRs for same REQ
100
123
  if: steps.closeout.outputs.changed == 'true' || steps.merge.outputs.merged == 'true'
101
124
  run: |
package/sdlc/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metasession.co/devaudit-sdlc",
3
- "version": "0.3.21",
3
+ "version": "0.3.23",
4
4
  "description": "DevAudit SDLC CLI Engine — cross-agent terminal-driven SDLC orchestrator.",
5
5
  "bin": {
6
6
  "devaudit-sdlc": "./src/bin/devaudit-sdlc.js"