-
Notifications
You must be signed in to change notification settings - Fork 7
Add OpenSSF Scorecard workflow to publish a public security score #94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,46 @@ | ||||||
| name: OpenSSF Scorecard | ||||||
|
|
||||||
| on: | ||||||
| branch_protection_rule: | ||||||
| schedule: | ||||||
| - cron: '31 5 * * 1' | ||||||
| push: | ||||||
| branches: [ main ] | ||||||
| workflow_dispatch: | ||||||
|
|
||||||
| permissions: read-all | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- workflow ---'
cat -n .github/workflows/scorecard.yml
printf '%s\n' '--- related workflow permission declarations ---'
rg -n -C 3 '(^|[[:space:]])permissions:|actions:|id-token:|security-events:' .github/workflowsRepository: qBraid/qbraid-algorithms Length of output: 3430 🌐 Web query:
💡 Result: In GitHub Actions, job-level permissions do not merge with or supplement workflow-level permissions; instead, they completely replace them for that specific job [1][2]. If you define a permissions block at the workflow level (such as permissions: read-all) and then define a separate permissions block within a specific job, the job-level configuration overrides the workflow-level configuration entirely for that job [2]. Key points regarding this behavior: - Total Replacement: When you add a permissions key to a job, the GITHUB_TOKEN for that job receives only the permissions you explicitly define in that job [1][3]. If a permission is omitted from the job-level block, it is set to none, even if it was granted at the workflow level [1][4][3]. - Security Best Practice: This override behavior allows you to follow the principle of least privilege [4][5]. You can set a restrictive default (like read-all) at the workflow level for general use, and then explicitly escalate permissions only for the specific jobs that require them [2]. - Calculation Order: GitHub determines the GITHUB_TOKEN permissions by first applying the repository/organization defaults, then applying the workflow-level permissions, and finally applying any job-level permissions [6]. Therefore, if you have permissions: read-all at the top level and need a specific job to have write access, you must restate all required permissions (including the read permissions you wish to maintain) within that job's permissions block [2]. Citations:
🏁 Script executed: #!/bin/bash
python3 - <<'PY'
from pathlib import Path
path = Path(".github/workflows/scorecard.yml")
lines = path.read_text().splitlines()
workflow_permissions = None
jobs = {}
in_jobs = False
current_job = None
in_job_permissions = False
for line in lines:
if not line.strip() or line.lstrip().startswith("#"):
continue
indent = len(line) - len(line.lstrip())
text = line.strip()
if indent == 0 and text.startswith("permissions:"):
workflow_permissions = text.split(":", 1)[1].strip() or "mapping"
elif indent == 0 and text == "jobs:":
in_jobs = True
current_job = None
in_job_permissions = False
elif in_jobs and indent == 2 and text.endswith(":"):
current_job = text[:-1]
jobs[current_job] = []
in_job_permissions = False
elif in_jobs and current_job and indent == 4 and text == "permissions:":
in_job_permissions = True
elif in_jobs and current_job and in_job_permissions and indent == 6:
jobs[current_job].append(text.split(":", 1)[0])
print(f"workflow_permissions={workflow_permissions!r}")
for job, permissions in jobs.items():
if permissions:
print(f"{job}: explicit permissions={permissions!r}; workflow default is replaced")
else:
print(f"{job}: no explicit permissions; workflow default would apply")
PYRepository: qBraid/qbraid-algorithms Length of output: 350 🏁 Script executed: #!/bin/bash
python3 - <<'PY'
from pathlib import Path
lines = Path(".github/workflows/scorecard.yml").read_text().splitlines()
workflow_permissions = None
jobs = {}
job = None
job_permissions = None
in_jobs = False
for raw in lines:
if not raw.strip() or raw.lstrip().startswith("#"):
continue
indent = len(raw) - len(raw.lstrip())
text = raw.strip()
if indent == 0 and text.startswith("permissions:"):
workflow_permissions = text.split(":", 1)[1].strip() or "mapping"
continue
if indent == 0 and text == "jobs:":
in_jobs = True
continue
if not in_jobs:
continue
if indent == 2 and text.endswith(":"):
job = text[:-1]
jobs[job] = None
job_permissions = None
continue
if job is None:
continue
if indent == 4 and text == "permissions:":
jobs[job] = []
job_permissions = jobs[job]
continue
if indent <= 4:
job_permissions = None
continue
if job_permissions is not None and indent == 6 and ":" in text:
job_permissions.append(text.split(":", 1)[0])
print(f"workflow_permissions={workflow_permissions!r}")
for name, permissions in jobs.items():
if permissions is None:
print(f"{name}: no explicit permissions; workflow default would apply")
else:
print(f"{name}: explicit permissions={permissions!r}; workflow default is replaced")
PYRepository: qBraid/qbraid-algorithms Length of output: 310 Set the workflow default permissions to none.
Proposed fix-permissions: read-all
+permissions: {}📝 Committable suggestion
Suggested change
🧰 Tools🪛 zizmor (1.29.0)[warning] 11-11: overly broad permissions (excessive-permissions): uses read-all permissions (excessive-permissions) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||
|
|
||||||
| jobs: | ||||||
| analysis: | ||||||
| name: Scorecard analysis | ||||||
| runs-on: ubuntu-latest | ||||||
| permissions: | ||||||
| security-events: write # upload SARIF to code scanning | ||||||
| id-token: write # publish results to the public Scorecard API | ||||||
| contents: read | ||||||
| actions: read | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Document the Add a comment that identifies the Scorecard check or behavior that requires 🧰 Tools🪛 zizmor (1.29.0)[warning] 21-21: permissions without explanatory comments (undocumented-permissions): needs an explanatory comment (undocumented-permissions) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||
|
|
||||||
| steps: | ||||||
| - name: Checkout | ||||||
| uses: actions/checkout@v4 | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- workflow ---'
cat -n .github/workflows/scorecard.yml
printf '%s\n' '--- action tag resolutions ---'
python3 - <<'PY'
import json
import urllib.request
refs = [
("actions", "checkout", "v4"),
("ossf", "scorecard-action", "v2"),
("actions", "upload-artifact", "v4"),
("github", "codeql-action", "v3"),
]
for owner, repo, tag in refs:
url = f"https://api.github.com/repos/{owner}/{repo}/git/ref/tags/{tag}"
req = urllib.request.Request(url, headers={"Accept": "application/vnd.github+json", "User-Agent": "review-verifier"})
with urllib.request.urlopen(req) as response:
data = json.load(response)
obj = data["object"]
result = {"repository": f"{owner}/{repo}", "tag": tag, "type": obj["type"], "sha": obj["sha"]}
if obj["type"] == "tag":
tag_url = f"https://api.github.com/repos/{owner}/{repo}/git/tags/{obj['sha']}"
tag_req = urllib.request.Request(tag_url, headers={"Accept": "application/vnd.github+json", "User-Agent": "review-verifier"})
with urllib.request.urlopen(tag_req) as response:
tag_data = json.load(response)
result["commit_sha"] = tag_data["object"]["sha"]
result["commit_type"] = tag_data["object"]["type"]
else:
result["commit_sha"] = obj["sha"]
print(json.dumps(result))
PYRepository: qBraid/qbraid-algorithms Length of output: 3873 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
import json
import subprocess
refs = [
("actions", "checkout", "v4"),
("ossf", "scorecard-action", "v2"),
("actions", "upload-artifact", "v4"),
("github", "codeql-action", "v3"),
]
def api(path):
raw = subprocess.check_output([
"curl", "-fsSk",
"-H", "Accept: application/vnd.github+json",
"-H", "User-Agent: review-verifier",
f"https://api.github.com/{path}",
])
return json.loads(raw)
for owner, repo, tag in refs:
ref = api(f"repos/{owner}/{repo}/git/ref/tags/{tag}")
obj = ref["object"]
tag_sha = obj["sha"]
if obj["type"] == "tag":
target = api(f"repos/{owner}/{repo}/git/tags/{tag_sha}")["object"]
commit_sha = target["sha"]
target_type = target["type"]
else:
commit_sha = tag_sha
target_type = obj["type"]
commit = api(f"repos/{owner}/{repo}/commits/{commit_sha}")
print(json.dumps({
"repository": f"{owner}/{repo}",
"tag": tag,
"tag_object_type": obj["type"],
"tag_object_sha": tag_sha,
"resolved_type": target_type,
"resolved_commit_sha": commit_sha,
"validated_commit_sha": commit["sha"],
}))
PYRepository: qBraid/qbraid-algorithms Length of output: 1170 🏁 Script executed: #!/bin/bash
set -u
for spec in \
"actions/checkout v4" \
"ossf/scorecard-action v2" \
"actions/upload-artifact v4" \
"github/codeql-action v3"
do
repo=${spec% *}
tag=${spec#* }
printf '\n--- %s @ %s ---\n' "$repo" "$tag"
git -c http.sslVerify=false ls-remote \
"https://github.com/$repo.git" \
"refs/tags/$tag" "refs/tags/$tag^{}" 2>&1 || true
printf '%s\n' '--- API tag ref ---'
curl -sSk \
-H 'Accept: application/vnd.github+json' \
-H 'User-Agent: review-verifier' \
-w '\nHTTP %{http_code}\n' \
"https://api.github.com/repos/$repo/git/ref/tags/$tag" || true
doneRepository: qBraid/qbraid-algorithms Length of output: 1758 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- scorecard-action version tags ---'
git -c http.sslVerify=false ls-remote --tags \
https://github.com/ossf/scorecard-action.git 2>&1 |
awk -F/ '$NF ~ /^v[0-9]/ {print}' |
tail -n 20
printf '%s\n' '--- commit validation ---'
for spec in \
"actions/checkout 11d5960a326750d5838078e36cf38b85af677262" \
"actions/upload-artifact ea165f8d65b6e75b540449e92b4886f43607fa02" \
"github/codeql-action f3712979fa5f215279b101dd0a2e3bdfb4353324"
do
repo=${spec% *}
sha=${spec#* }
printf '%s @ %s: ' "$repo" "$sha"
curl -sSk \
-H 'Accept: application/vnd.github+json' \
-H 'User-Agent: review-verifier' \
-w ' HTTP %{http_code}\n' \
"https://api.github.com/repos/$repo/commits/$sha" |
sed -n '1p'
doneRepository: qBraid/qbraid-algorithms Length of output: 50383 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- scorecard-action tags ---'
git -c http.sslVerify=false ls-remote --tags \
https://github.com/ossf/scorecard-action.git 2>/dev/null |
grep -E 'refs/tags/v[0-9]+(\.[0-9]+){0,2}(\^\{\})?$' |
tail -n 30
printf '%s\n' '--- validated commit status ---'
for spec in \
"actions/checkout 11d5960a326750d5838078e36cf38b85af677262" \
"actions/upload-artifact ea165f8d65b6e75b540449e92b4886f43607fa02" \
"github/codeql-action f3712979fa5f215279b101dd0a2e3bdfb4353324"
do
repo=${spec% *}
sha=${spec#* }
status=$(curl -sSk -o /dev/null -w '%{http_code}' \
-H 'Accept: application/vnd.github+json' \
-H 'User-Agent: review-verifier' \
"https://api.github.com/repos/$repo/commits/$sha")
printf '%s %s %s\n' "$repo" "$sha" "$status"
doneRepository: qBraid/qbraid-algorithms Length of output: 2203 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- scorecard v2 branch ---'
git -c http.sslVerify=false ls-remote \
https://github.com/ossf/scorecard-action.git \
refs/heads/v2 2>/dev/null
printf '%s\n' '--- scorecard v2.4.4 dereference ---'
git -c http.sslVerify=false ls-remote \
https://github.com/ossf/scorecard-action.git \
refs/tags/v2.4.4 refs/tags/v2.4.4^{} 2>/dev/nullRepository: qBraid/qbraid-algorithms Length of output: 346 🏁 Script executed: #!/bin/bash
set -u
repo=ossf/scorecard-action
sha=2d1146689b8cda280b9bc96326124645441f03bc
curl -sSk \
-H 'Accept: application/vnd.github+json' \
-H 'User-Agent: review-verifier' \
-w '\nHTTP %{http_code}\n' \
"https://api.github.com/repos/$repo/commits/$sha" |
grep -E '"sha":|HTTP 'Repository: qBraid/qbraid-algorithms Length of output: 5467 Pin all workflow actions to full commit SHAs. Use verified immutable references:
🧰 Tools🪛 zizmor (1.29.0)[error] 25-25: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||
| with: | ||||||
| persist-credentials: false | ||||||
|
|
||||||
| - name: Run analysis | ||||||
| uses: ossf/scorecard-action@v2 | ||||||
| with: | ||||||
| results_file: results.sarif | ||||||
| results_format: sarif | ||||||
| publish_results: true | ||||||
|
|
||||||
| - name: Upload artifact | ||||||
| uses: actions/upload-artifact@v4 | ||||||
| with: | ||||||
| name: SARIF file | ||||||
| path: results.sarif | ||||||
| retention-days: 5 | ||||||
|
|
||||||
| - name: Upload to code scanning | ||||||
| uses: github/codeql-action/upload-sarif@v3 | ||||||
| with: | ||||||
| sarif_file: results.sarif | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win
Serialize Scorecard runs.
The
push,schedule,branch_protection_rule, andworkflow_dispatchtriggers can start overlapping jobs. Concurrent runs can create redundant or out-of-order SARIF and Scorecard publications.Add a stable concurrency group. For latest-state reporting, use:
🧰 Tools
🪛 YAMLlint (1.37.1)
[warning] 3-3: truthy value should be one of [false, true]
(truthy)
[error] 8-8: too many spaces inside brackets
(brackets)
[error] 8-8: too many spaces inside brackets
(brackets)
🪛 zizmor (1.29.0)
[warning] 3-9: insufficient job-level concurrency limits (concurrency-limits): workflow is missing concurrency setting
(concurrency-limits)
🤖 Prompt for AI Agents
Source: Linters/SAST tools