Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions .github/workflows/harbor-benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ name: Harbor Benchmark

on:
workflow_dispatch:
inputs:
cli-version:
description: Version of the SonarQube CLI under test, written in the release report
default: unknown
iteration:
description: Iteration name written in the release report, for example 26Q4-4 (optional)
default: ""
store-report:
description: Store the release report on the benchmark-data branch and build the dashboard
type: boolean
default: false

permissions:
id-token: write
Expand Down Expand Up @@ -168,6 +179,29 @@ jobs:
if: ${{ !cancelled() && hashFiles('report.txt') != '' }}
run: tee -a "$GITHUB_STEP_SUMMARY" < report.txt

- name: Build the release report
id: release-report
if: ${{ !cancelled() && hashFiles('harbor-jobs/**') != '' }}
shell: bash
env:
CLI_VERSION: ${{ inputs.cli-version }}
ITERATION: ${{ inputs.iteration }}
run: |
iteration=()
[ -n "${ITERATION}" ] && iteration=(--iteration "${ITERATION}")
mkdir release-report
bun benchmarks/harbor/report/to-json.ts \
--cli-version "${CLI_VERSION}" --run-id "${GITHUB_RUN_ID}" --commit "${GITHUB_SHA}" \
--date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" ${iteration[@]+"${iteration[@]}"} \
harbor-jobs > "release-report/${GITHUB_RUN_ID}.json"
Comment on lines +182 to +196

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Security: Release JSON and dashboard are published without the secrets scan

The README sets the rule "Nothing is published before it is scanned". It also says the report job scans its report text, even though that text is built only from result.json fields and numbers taken from artifacts that were already scanned. The new release JSON comes from the same fields: task names, agent and model names, config.json model names and trial folder names. Yet it is uploaded as the release-report artifact and pushed to a durable external repository, and dashboard.html is uploaded too, without ci/scan-secrets.py running on either. This bypasses the scan the README describes, and the data store may become public (see open points). Fix: run python3 -I benchmarks/harbor/ci/scan-secrets.py release-report before the upload, the same way report.txt is scanned.

Was this helpful? React with 👍 / 👎

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not changed. The release JSON is built only from task, agent and model names and from numbers in result.json and trajectory.json final_metrics, never from message text, so I see no way for a secret to reach it. The artifacts it reads were already scanned and redacted by the benchmark jobs. I would rather not add a scan step to a PoC for this.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Checked benchmarks/harbor/report/to-json.ts. The output has no message text: the only free-form strings are task, agent and model, and everything else is a number or a fixed status. The leak risk is lower than the finding suggests.

One gap remains. On the crash path (crashedTrialRecord, lines 100-109), task comes from the trial folder name and agent/model from config.json. Those files were not read from result.json, and the agent may be able to write them. The README says the artifacts were "already scanned and redacted", but a value in a field that no scan flagged would still be published to the external repository and the dashboard.

If you accept that small risk for the PoC, the finding can stay as a known limitation. Running python3 -I benchmarks/harbor/ci/scan-secrets.py release-report before the upload is a one-line step if you change your mind. I made no code changes.


- name: Upload the release report
if: ${{ !cancelled() && steps.release-report.outcome == 'success' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-report
path: release-report

- name: Fail when a trial did not pass
if: ${{ !cancelled() && steps.summarize.outputs.status != '0' }}
run: exit 1
Expand All @@ -177,3 +211,56 @@ jobs:
run: |
echo "A benchmark job finished with: ${{ needs.benchmark.result }}"
exit 1

store:
name: Store the release report
runs-on: sonar-xs
needs: report
if: ${{ !cancelled() && inputs.store-report }}
permissions:
id-token: write
contents: read
concurrency:
group: benchmark-data
Comment thread
gitar-bot[bot] marked this conversation as resolved.
Resolved
queue: max
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Vault Secrets
id: secrets
uses: SonarSource/vault-action-wrapper@a7fa5fd4a4e4e0f2593f6137c0812cac196cf4ce # 3.7.0
with:
secrets: |
development/artifactory/token/{REPO_OWNER_NAME_DASH}-private-reader username | ARTIFACTORY_USER;
development/artifactory/token/{REPO_OWNER_NAME_DASH}-private-reader access_token | ARTIFACTORY_ACCESS_TOKEN;

- uses: ./.github/actions/setup-bun
with:
artifactory-username: ${{ fromJSON(steps.secrets.outputs.vault).ARTIFACTORY_USER }}
artifactory-password: ${{ fromJSON(steps.secrets.outputs.vault).ARTIFACTORY_ACCESS_TOKEN }}

- name: Download the release report
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-report
path: release-report

- name: Store the release report
env:
BENCHMARK_DATA_REMOTE: https://x-access-token:${{ secrets.HB_BENCHMARK_DATA_TOKEN }}@github.com/jonathan-balli-sonarsource/harbor-benchmark-data-poc.git
run: bash benchmarks/harbor/report/adapters/git-branch.sh push "release-report/${GITHUB_RUN_ID}.json"

- name: Build the dashboard
env:
BENCHMARK_DATA_REMOTE: https://x-access-token:${{ secrets.HB_BENCHMARK_DATA_TOKEN }}@github.com/jonathan-balli-sonarsource/harbor-benchmark-data-poc.git
run: |
bash benchmarks/harbor/report/adapters/git-branch.sh pull release-data
bun benchmarks/harbor/report/dashboard.ts release-data > dashboard.html

- name: Upload the dashboard
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: harbor-dashboard
path: dashboard.html
2 changes: 2 additions & 0 deletions benchmarks/harbor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ In CI both Linux and macOS run without containers: the runner is the sandbox. Th
The report takes pass/fail from the verifier reward (`1` = pass) and tokens, cost and steps from the ATIF `agent/trajectory.json` of each trial. The exit code is non-zero when any task is not `PASS`.

Nothing is published before it is scanned. After `run-isolated.sh`, each benchmark job runs `ci/scan-secrets.py` over the collected `harbor-jobs` directory, as the runner user and outside the agent's sandbox, and the report job runs it over the report text before it reaches the run summary. The script scans a copy of the files in which every name gets a `.txt` suffix (the scanner skips `.md` and `.html` by default) and the values listed in `ci/known-secrets.txt` are replaced by a filler of the same length. It runs the `sonar-secrets` binary that `sonar analyze secrets` installs, with `--max-file-size=0` and `--disable-test-file-detection`, because by default the scanner silently skips files over 1 MB and files that look like tests. `ci/known-secrets.txt` holds one public fake value per line, the ones the `secrets-context--*` tasks plant on purpose: add a line there when a task plants a new one. Any other finding is a leak. The script replaces the reported ranges, and the literal values of `SONARQUBE_CLI_TOKEN` and `PORTKEY_API_KEY` with `[REDACTED]`, scans again, and removes every file that still has a finding. A leak always fails the job, and `secrets-leak.json` next to the trial makes the report show `LEAK` instead of `PASS`. The log shows only file, line and rule key. The CLI is installed in a separate step without secrets. The benchmark-job scan step has the Sonar token from the Vault path `development/kv/data/devex-harbor-sqc` (as `SONARQUBE_CLI_TOKEN`) and the Portkey key of the matrix agent (the Claude key from the Vault path `development/kv/data/devex-harbor-claude` for every agent but `codex`, the key from the Vault path `development/kv/data/devex-harbor-codex` for `codex`, as `PORTKEY_API_KEY`, the same expression as in the run step) in its own `env:`. Every agent key must reach this step, because the agent can read its key and the detector does not know Portkey keys, so only the exact value is redacted: when you add an agent with its own key, add it to the `PORTKEY_API_KEY` expression of both steps (or to `CREDENTIAL_ENV_VARS` in `ci/scan-secrets.py` when it uses a different variable). The report-job scan step has only the token, read from the same `devex-harbor-sqc` path, because the report job reads artifacts that were already scanned and redacted and builds the text from `result.json` fields and numbers, so only the report text is scanned there and no agent key is needed. Both set `DO_NOT_TRACK=1`.

The release report turns a run into data. `report/to-json.ts <runDir>` writes one JSON per run (one line per trial) from the `harbor-jobs` directory of the report job, which holds one folder per system, and `report/dashboard.ts <dataDir>` builds a standalone HTML dashboard from a directory of those JSON. The `store-report` input of the workflow pushes the JSON through `report/adapters/<adapter>.sh push <file>` and reads them back with `pull <dir>`: `git-branch.sh` keeps them on an orphan branch, and another storage only needs another adapter.
56 changes: 56 additions & 0 deletions benchmarks/harbor/report/adapters/git-branch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/bin/bash
set -euo pipefail

usage="usage: git-branch.sh pull <dir> | push <file.json> (BENCHMARK_DATA_REMOTE is the repository, BENCHMARK_DATA_BRANCH the branch, default benchmark-data)"
remote="${BENCHMARK_DATA_REMOTE:?${usage}}"
branch="${BENCHMARK_DATA_BRANCH:-benchmark-data}"
action="${1:?${usage}}"
target="${2:?${usage}}"

export GIT_TERMINAL_PROMPT=0
work="$(mktemp -d)"
trap 'rm -rf "${work}"' EXIT
git init -q "${work}"
git -C "${work}" remote add origin "${remote}"

branch_exists() {
local status=0
git -C "${work}" ls-remote --exit-code --heads origin "${branch}" > /dev/null || status=$?
case "${status}" in
0) return 0 ;;
2) return 1 ;;
*) echo "Cannot reach '${remote}'" >&2; exit 1 ;;
esac
}

case "${action}" in
pull)
branch_exists || { echo "No branch '${branch}' in '${remote}'" >&2; exit 1; }
git -C "${work}" fetch -q --depth 1 origin "${branch}"
git -C "${work}" checkout -q FETCH_HEAD
mkdir -p "${target}"
find "${work}" -maxdepth 1 -name '*.json' -exec cp {} "${target}" \;
;;
push)
[[ -f "${target}" ]] || { echo "No file '${target}'" >&2; exit 1; }
if branch_exists; then
git -C "${work}" fetch -q --depth 1 origin "${branch}"
git -C "${work}" checkout -q -B "${branch}" FETCH_HEAD
else
git -C "${work}" checkout -q --orphan "${branch}"
fi
cp "${target}" "${work}/"
git -C "${work}" add -- "$(basename "${target}")"
if git -C "${work}" diff --cached --quiet; then
echo "$(basename "${target}") is already stored" >&2
exit 0
fi
git -C "${work}" -c user.name=harbor-benchmark -c user.email=harbor-benchmark@users.noreply.github.com \
commit -q -m "Add $(basename "${target}")"
git -C "${work}" push -q origin "HEAD:refs/heads/${branch}"
;;
*)
echo "${usage}" >&2
exit 1
;;
esac
24 changes: 24 additions & 0 deletions benchmarks/harbor/report/adapters/local-dir.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash
set -euo pipefail

usage="usage: local-dir.sh pull <dir> | push <file.json> (BENCHMARK_DATA_DIR is the storage folder)"
store="${BENCHMARK_DATA_DIR:?${usage}}"
action="${1:?${usage}}"
target="${2:?${usage}}"

case "${action}" in
pull)
[[ -d "${store}" ]] || { echo "No storage folder '${store}'" >&2; exit 1; }
mkdir -p "${target}"
find "${store}" -maxdepth 1 -name '*.json' -exec cp {} "${target}" \;
;;
push)
[[ -f "${target}" ]] || { echo "No file '${target}'" >&2; exit 1; }
mkdir -p "${store}"
cp "${target}" "${store}/"
;;
*)
echo "${usage}" >&2
exit 1
;;
esac
Loading
Loading