Offer re-encrypting the database with stronger encryption if a vault … #68
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Security | |
| # The security report: every check that says something about whether this | |
| # build can be trusted, in one run with one summary. Pushes and pull requests | |
| # get the fast pass; the weekly schedule and a manual run raise the fuzz | |
| # budget by two orders of magnitude and go through the packaged binary. | |
| # | |
| # Jobs: | |
| # fuzz property-based fuzzing of every parser at a trust boundary | |
| # (tests/fuzz) plus the hardening invariants pinned in | |
| # tests/hardening-invariants.test.ts | |
| # native-fuzz libFuzzer over the C++ addons' own parsing (electron/native), | |
| # under AddressSanitizer and UndefinedBehaviorSanitizer: no | |
| # bounds checks there, and what they read comes from a driver, | |
| # a removable device and the keychain | |
| # codeql GitHub's static analysis, results land in the Security tab | |
| # audit the lockfile against the OSV advisory database | |
| # dependency-review on pull requests: new dependencies with known | |
| # vulnerabilities or unacceptable licenses block the merge | |
| # packaged builds the Linux app and checks the bytes that ship: the | |
| # fuse wire matches the config, the app boots and answers IPC | |
| # reproducible builds the app twice, on different runner images and from | |
| # different checkout paths, and fails unless both produce | |
| # the same app.asar byte for byte | |
| # | |
| # Findings from fuzz, invariants, the native fuzzer, fuses and boot are | |
| # uploaded as SARIF, so | |
| # they appear as code scanning alerts in the Security tab beside CodeQL's | |
| # and resolve on their own once a later run no longer reports them. | |
| # Dependency alerts come from Dependabot, a repository setting rather than | |
| # a workflow, and live in the same tab. There is no dependabot.yml on | |
| # purpose: version-update pull requests are not wanted, and alerts do not | |
| # need one. | |
| on: | |
| push: | |
| branches: [ main, develop ] | |
| pull_request: | |
| branches: [ main, develop ] | |
| schedule: | |
| # Mondays, 06:00 UTC | |
| - cron: '0 6 * * 1' | |
| workflow_dispatch: | |
| inputs: | |
| fuzz_runs: | |
| description: 'Iterations per fuzz property' | |
| required: false | |
| default: '20000' | |
| native_fuzz_seconds: | |
| description: 'Seconds of native (libFuzzer) fuzzing' | |
| required: false | |
| default: '900' | |
| permissions: | |
| contents: read | |
| env: | |
| # Deep on the schedule and on demand; a push gets a budget that keeps the | |
| # job under a few minutes but still runs every property | |
| FUZZ_RUNS: ${{ github.event_name == 'schedule' && '20000' || github.event.inputs.fuzz_runs || '2000' }} | |
| # The native target runs millions of inputs a second, so a push still gets | |
| # a real pass; the schedule gets a quarter of an hour | |
| NATIVE_FUZZ_SECONDS: ${{ github.event_name == 'schedule' && '900' || github.event.inputs.native_fuzz_seconds || '60' }} | |
| jobs: | |
| fuzz: | |
| name: Fuzz and hardening invariants | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci --no-audit --no-fund | |
| # Both runs write a JSON report alongside the console output; a failure | |
| # is recorded and the job carries on so the report reaches the Security | |
| # tab, then fails at the end | |
| - name: Hardening invariants | |
| id: invariants | |
| run: npx vitest run tests/hardening-invariants.test.ts --reporter=verbose --reporter=json --outputFile=invariants.json --includeTaskLocation | |
| continue-on-error: true | |
| - name: Fuzz | |
| id: fuzz | |
| run: | | |
| echo "fuzz budget: $FUZZ_RUNS runs per property" | |
| npx vitest run tests/fuzz --reporter=verbose --reporter=json --outputFile=fuzz.json --includeTaskLocation 2>&1 | tee fuzz.log | |
| exit "${PIPESTATUS[0]}" | |
| continue-on-error: true | |
| - name: Convert findings to SARIF | |
| if: always() | |
| run: node scripts/sarif-report.mjs vitest invariants.json fuzz.json --out fuzz.sarif | |
| # Alerts land under Security > Code scanning with this category; an | |
| # upload with no results resolves the previous run's alerts. Forks | |
| # cannot write security events, so the upload is allowed to fail there | |
| - name: Upload to the Security tab | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@486fec2a3ea2626afcd8c7e9208b4f515078dd7e # codeql-bundle-v2.26.4 | |
| with: | |
| sarif_file: fuzz.sarif | |
| category: fuzz-and-invariants | |
| continue-on-error: true | |
| - name: Report | |
| if: always() | |
| run: | | |
| { | |
| echo "## Fuzz and hardening invariants" | |
| echo | |
| echo "Budget: \`$FUZZ_RUNS\` runs per property. Seeds are printed on failure; reproduce with \`FUZZ_SEED=<seed> FUZZ_RUNS=$FUZZ_RUNS npx vitest run <file>\`. Failures are also filed under Security > Code scanning, category \`fuzz-and-invariants\`." | |
| echo | |
| echo '```' | |
| grep -E "✓|✗|×|Test Files|Tests |Counterexample|seed" fuzz.log | sed 's/\x1b\[[0-9;]*m//g' | head -120 | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Fail on findings | |
| if: steps.invariants.outcome == 'failure' || steps.fuzz.outcome == 'failure' | |
| env: | |
| INVARIANTS: ${{ steps.invariants.outcome }} | |
| FUZZ: ${{ steps.fuzz.outcome }} | |
| run: | | |
| echo "hardening invariants: $INVARIANTS, fuzz: $FUZZ" | |
| exit 1 | |
| # Both runners: the Touch ID addon's string handling is Objective-C, so the | |
| # macOS leg is the only one that fuzzes it rather than compiling it out. | |
| # Apple's clang has no libFuzzer; the runner uses the Homebrew LLVM the | |
| # image ships instead. Every instrumentation it tries is probed by running | |
| # it, since on macOS 26 that LLVM's AddressSanitizer builds a binary that | |
| # hangs at startup; the run reports which sanitizers it ended up with | |
| native-fuzz: | |
| name: Native addon fuzz (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| security-events: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '22' | |
| # The image ships a Homebrew LLVM (llvm@20 on macOS 26, llvm@18 on 15); | |
| # this only installs one if an image change ever drops it | |
| - name: Ensure a libFuzzer-capable clang | |
| if: runner.os == 'macOS' | |
| run: ls "$(brew --prefix)/opt" | grep -q '^llvm' || brew install llvm | |
| # The targets include nothing but the addons' own headers, so neither | |
| # pcsclite nor node-gyp is needed here | |
| - name: Fuzz the native addons | |
| id: native | |
| run: | | |
| echo "native fuzz budget: ${NATIVE_FUZZ_SECONDS}s per target" | |
| node scripts/fuzz-native.mjs --seconds "$NATIVE_FUZZ_SECONDS" | |
| continue-on-error: true | |
| - name: Convert findings to SARIF | |
| if: always() | |
| run: node scripts/sarif-report.mjs native native-fuzz-out/pcsc.log native-fuzz-out/touchid.log --out native-fuzz.sarif | |
| # A category per runner: an upload replaces the previous one under the | |
| # same name, so one shared category would resolve the other leg's alerts | |
| - name: Upload to the Security tab | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@486fec2a3ea2626afcd8c7e9208b4f515078dd7e # codeql-bundle-v2.26.4 | |
| with: | |
| sarif_file: native-fuzz.sarif | |
| category: native-fuzz-${{ matrix.os }} | |
| continue-on-error: true | |
| # The input that reproduced it is the whole reproduction, and it is a few | |
| # bytes; without it the stack in the log is all anyone has to go on | |
| - name: Keep the reproducer | |
| if: steps.native.outcome == 'failure' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: native-fuzz-reproducer-${{ matrix.os }} | |
| path: | | |
| native-fuzz-out/*/crash-* | |
| native-fuzz-out/*/leak-* | |
| native-fuzz-out/*/timeout-* | |
| native-fuzz-out/*/oom-* | |
| native-fuzz-out/*.log | |
| if-no-files-found: ignore | |
| # The runner name reaches the script through env, where the shell quotes | |
| # it; substituted into the script it would be code | |
| - name: Report | |
| if: always() | |
| env: | |
| RUNNER_OS_LABEL: ${{ matrix.os }} | |
| run: | | |
| { | |
| echo "## Native addon fuzz ($RUNNER_OS_LABEL)" | |
| echo | |
| echo "Budget: \`${NATIVE_FUZZ_SECONDS}\`s per target under AddressSanitizer and UndefinedBehaviorSanitizer. Findings are filed under Security > Code scanning, category \`native-fuzz-$RUNNER_OS_LABEL\`, and the reproducing input is attached to the run." | |
| echo | |
| echo '```' | |
| grep -hE "^compiler:|^mode:|^instrumentation:|^passed over:|ERROR|SUMMARY|runtime error|Assertion|^Done |^replayed |stat::number_of_executed_units" native-fuzz-out/*.log 2>/dev/null | head -40 || echo "no log" | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Fail on findings | |
| if: steps.native.outcome == 'failure' | |
| run: exit 1 | |
| codeql: | |
| name: CodeQL | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| security-events: write | |
| actions: read | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Initialize CodeQL | |
| uses: github/codeql-action/init@486fec2a3ea2626afcd8c7e9208b4f515078dd7e # codeql-bundle-v2.26.4 | |
| with: | |
| languages: javascript-typescript | |
| queries: security-extended | |
| config-file: ./.github/codeql/config.yml | |
| - name: Analyze | |
| uses: github/codeql-action/analyze@486fec2a3ea2626afcd8c7e9208b4f515078dd7e # codeql-bundle-v2.26.4 | |
| with: | |
| category: /language:javascript-typescript | |
| - name: Report | |
| if: always() | |
| run: | | |
| { | |
| echo "## CodeQL" | |
| echo | |
| echo "Findings, if any, are in the Security tab under Code scanning (category \`/language:javascript-typescript\`, query suite \`security-extended\`)." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| audit: | |
| name: Dependency audit | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| # OSV carries the same advisories npm audit reads (GitHub's database | |
| # among its sources) behind its own API, so an npm registry outage no | |
| # longer fails the report or stalls it for minutes. The whole lockfile | |
| # is scanned, build tooling included: a compromised bundler ships as | |
| # surely as a compromised runtime dependency | |
| - name: Scan the lockfile against OSV | |
| id: osv | |
| uses: google/osv-scanner-action/osv-scanner-action@6e4298ebc4db23e847df9b2e2de2939d6f066c67 # v2.5.1 | |
| with: | |
| scan-args: |- | |
| --lockfile=package-lock.json | |
| --format=sarif | |
| --output=osv.sarif | |
| continue-on-error: true | |
| - name: Upload to the Security tab | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@486fec2a3ea2626afcd8c7e9208b4f515078dd7e # codeql-bundle-v2.26.4 | |
| with: | |
| sarif_file: osv.sarif | |
| category: dependencies | |
| continue-on-error: true | |
| - name: Report | |
| if: always() | |
| run: | | |
| node -e ' | |
| const fs = require("fs"); | |
| const lines = ["## Dependency audit (OSV)", ""]; | |
| if (!fs.existsSync("osv.sarif")) { | |
| lines.push("The scanner produced no report; see the scan step."); | |
| } else { | |
| const results = JSON.parse(fs.readFileSync("osv.sarif", "utf8")).runs.flatMap(r => r.results ?? []); | |
| lines.push(results.length === 0 | |
| ? "No known vulnerabilities in package-lock.json." | |
| : `${results.length} finding(s), filed under Security > Code scanning, category \`dependencies\`:`); | |
| for (const r of results.slice(0, 40)) lines.push(`- ${r.ruleId}: ${(r.message?.text ?? "").split("\n")[0].slice(0, 160)}`); | |
| } | |
| fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, lines.join("\n") + "\n\n"); | |
| ' | |
| - name: Fail on findings | |
| if: steps.osv.outcome == 'failure' | |
| run: exit 1 | |
| dependency-review: | |
| name: Dependency review | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Review dependency changes | |
| uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 | |
| with: | |
| fail-on-severity: high | |
| comment-summary-in-pr: on-failure | |
| packaged: | |
| name: Packaged binary | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci --no-audit --no-fund | |
| - name: Install Linux dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libnss3 libsecret-1-dev libusb-1.0-0-dev libudev-dev libpcsclite-dev pkgconf xvfb | |
| # An unpacked directory is enough to inspect and boot; the AppImage | |
| # wrapping adds minutes and changes nothing these checks look at | |
| - name: Package | |
| run: | | |
| npx tsc -b | |
| npx vite build | |
| node electron/build.mjs | |
| node electron/copy-native-modules.mjs | |
| npx electron-builder --config electron-builder.config.js --linux dir --publish never | |
| env: | |
| NODE_ENV: production | |
| - name: Fuse wire matches the configuration | |
| id: fuses | |
| run: | | |
| node scripts/check-fuses.mjs 2>&1 | tee fuses.log | |
| exit "${PIPESTATUS[0]}" | |
| continue-on-error: true | |
| - name: Boot smoke test | |
| id: smoke | |
| run: | | |
| xvfb-run -a node scripts/smoke-boot.mjs -- --ozone-platform=x11 2>&1 | tee smoke.log | |
| exit "${PIPESTATUS[0]}" | |
| continue-on-error: true | |
| - name: Convert findings to SARIF | |
| if: always() | |
| run: | | |
| node scripts/sarif-report.mjs fuses fuses.log --out fuses.sarif | |
| node scripts/sarif-report.mjs smoke smoke.log --out smoke.sarif | |
| - name: Upload fuse findings to the Security tab | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@486fec2a3ea2626afcd8c7e9208b4f515078dd7e # codeql-bundle-v2.26.4 | |
| with: | |
| sarif_file: fuses.sarif | |
| category: packaged-fuses | |
| continue-on-error: true | |
| - name: Upload boot findings to the Security tab | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@486fec2a3ea2626afcd8c7e9208b4f515078dd7e # codeql-bundle-v2.26.4 | |
| with: | |
| sarif_file: smoke.sarif | |
| category: packaged-boot | |
| continue-on-error: true | |
| - name: Report | |
| if: always() | |
| run: | | |
| { | |
| echo "## Packaged binary" | |
| echo | |
| echo "Failures are also filed under Security > Code scanning, categories \`packaged-fuses\` and \`packaged-boot\`." | |
| echo | |
| echo '```' | |
| cat fuses.log 2>/dev/null || echo "fuse check did not run" | |
| echo | |
| cat smoke.log 2>/dev/null || echo "smoke test did not run" | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Fail on findings | |
| if: steps.fuses.outcome == 'failure' || steps.smoke.outcome == 'failure' | |
| env: | |
| FUSES: ${{ steps.fuses.outcome }} | |
| BOOT: ${{ steps.smoke.outcome }} | |
| run: | | |
| echo "fuses: $FUSES, boot: $BOOT" | |
| exit 1 | |
| reproducible: | |
| name: Reproducible asar (${{ matrix.image }}) | |
| runs-on: ${{ matrix.image }} | |
| timeout-minutes: 30 | |
| strategy: | |
| matrix: | |
| include: | |
| # Two images and two working directories: a hash that survives | |
| # both is one a third party can expect to reproduce | |
| - image: ubuntu-latest | |
| checkout: . | |
| - image: ubuntu-24.04 | |
| checkout: elsewhere/vigil-rebuild | |
| defaults: | |
| run: | |
| working-directory: ${{ matrix.checkout }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| path: ${{ matrix.checkout }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| cache-dependency-path: ${{ matrix.checkout }}/package-lock.json | |
| - name: Install dependencies | |
| run: npm ci --no-audit --no-fund | |
| - name: Install Linux dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libnss3 libsecret-1-dev libusb-1.0-0-dev libudev-dev libpcsclite-dev pkgconf | |
| - name: Package | |
| run: | | |
| npx tsc -b | |
| npx vite build | |
| node electron/build.mjs | |
| node electron/copy-native-modules.mjs | |
| npx electron-builder --config electron-builder.config.js --linux dir --publish never | |
| env: | |
| NODE_ENV: production | |
| - name: Hash the asar | |
| run: node scripts/asar-hash.mjs dist --write asar.sha256 | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: asar-hash-${{ matrix.image }} | |
| path: ${{ matrix.checkout }}/dist/asar.sha256 | |
| if-no-files-found: error | |
| reproducible-compare: | |
| name: Reproducible asar (compare) | |
| needs: reproducible | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: asar-hash-* | |
| path: hashes | |
| - name: Compare | |
| run: | | |
| find hashes -name asar.sha256 -exec cat {} + | |
| distinct=$(find hashes -name asar.sha256 -exec cat {} + | sort -u | wc -l) | |
| { | |
| echo "## Reproducible asar" | |
| echo | |
| echo '```' | |
| find hashes -name asar.sha256 -exec cat {} + | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| if [ "$distinct" != "1" ]; then | |
| echo "the two builds produced different app.asar files" | |
| exit 1 | |
| fi |