Skip to content
Merged
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
159 changes: 159 additions & 0 deletions .github/workflows/daily-security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
name: Daily security audit

on:
schedule:
# 06:00 UTC — after the previous day's advisory databases have settled.
- cron: "0 6 * * *"
workflow_dispatch:

# A newer run on main cancels an in-flight audit so we never double-tag.
concurrency:
group: daily-security
cancel-in-progress: true

permissions:
contents: write
issues: write
actions: write

jobs:
audit:
name: Audit, remediate, release
runs-on: ubuntu-22.04
# Schedule only fires on the default branch. Manual runs are allowed from
# any ref so the scan can be tested; tagging still requires main.
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: npm

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: "./src-tauri -> target"

- name: Install frontend dependencies
run: npm ci

- name: Install cargo-audit
uses: taiki-e/install-action@v2
with:
tool: cargo-audit

- name: Scan and apply safe remediations
id: scan
run: node scripts/security-release.mjs --apply

- name: Job summary
if: always() && steps.scan.outcome == 'success'
env:
REMAINING: ${{ steps.scan.outputs.remaining }}
NOTES: ${{ steps.scan.outputs.notes }}
run: |
{
echo "## Security audit"
echo ""
echo "- remediated: \`${{ steps.scan.outputs.remediated }}\`"
echo "- audit_ok: \`${{ steps.scan.outputs.audit_ok }}\`"
echo "- version: \`${{ steps.scan.outputs.version }}\`"
if [ -n "${REMAINING}" ]; then
echo ""
echo "Outstanding:"
echo ""
echo '```'
printf '%s\n' "${REMAINING}"
echo '```'
fi
if [ -n "${NOTES}" ]; then
echo ""
echo "Release notes:"
echo ""
echo "${NOTES}"
fi
} >> "$GITHUB_STEP_SUMMARY"

- name: Unit tests
if: steps.scan.outputs.remediated == 'true' && steps.scan.outputs.audit_ok == 'true'
run: npm test

- name: Typecheck & build frontend
if: steps.scan.outputs.remediated == 'true' && steps.scan.outputs.audit_ok == 'true'
run: npm run build

- name: Commit, tag, and dispatch draft release
if: github.ref == 'refs/heads/main' && steps.scan.outputs.remediated == 'true' && steps.scan.outputs.audit_ok == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.scan.outputs.version }}
NOTES: ${{ steps.scan.outputs.notes }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

git add package.json package-lock.json \
src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.conf.json
git status --short
git commit -m "Release ${VERSION}: security dependency updates"
git pull --rebase origin main
git tag "v${VERSION}"
git push origin HEAD:main
git push origin "v${VERSION}"

# Pushing a tag with GITHUB_TOKEN does not trigger other workflows,
# so dispatch the existing Release workflow on the new tag.
node -e '
const fs = require("fs");
fs.writeFileSync("/tmp/release-dispatch.json", JSON.stringify({
ref: "v" + process.env.VERSION,
inputs: { notes: process.env.NOTES || "" },
}));
'
gh api -X POST "repos/${GITHUB_REPOSITORY}/actions/workflows/release.yml/dispatches" \
--input /tmp/release-dispatch.json

- name: Open or update tracking issue
if: github.ref == 'refs/heads/main' && steps.scan.outputs.audit_ok != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REMAINING: ${{ steps.scan.outputs.remaining }}
run: |
set -euo pipefail
TITLE="Outstanding dependency vulnerabilities"
BODY="$(cat <<EOF
The daily security audit found high/critical advisories that could not be auto-remediated.

\`\`\`
${REMAINING}
\`\`\`

Allowlisted findings (see \`audit-ci.jsonc\`) are ignored. Once a patched release exists, this job will apply it and open a draft GitHub Release.
EOF
)"
EXISTING="$(gh issue list --state open --search "in:title ${TITLE}" --json number --jq '.[0].number // empty')"
if [ -n "${EXISTING}" ]; then
gh issue comment "${EXISTING}" --body "${BODY}"
else
gh issue create --title "${TITLE}" --body "${BODY}"
fi

- name: Close tracking issue when clean
if: github.ref == 'refs/heads/main' && steps.scan.outputs.audit_ok == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
TITLE="Outstanding dependency vulnerabilities"
EXISTING="$(gh issue list --state open --search "in:title ${TITLE}" --json number --jq '.[0].number // empty')"
if [ -n "${EXISTING}" ]; then
gh issue close "${EXISTING}" --reason completed --comment "The daily audit is clean again (allowlisted findings only)."
fi
23 changes: 21 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ on:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Existing tag to release (e.g. v0.7.2). Leave empty to use the current ref when it is a tag."
required: false
type: string
notes:
description: "Release notes for the draft GitHub Release"
required: false
type: string

jobs:
# Create the draft release once, up front, so the platform build jobs upload to
Expand All @@ -22,13 +31,21 @@ jobs:
uses: actions/github-script@v7
with:
script: |
const tag = context.ref.replace('refs/tags/', '');
const inputTag = context.payload.inputs?.tag;
const tag = inputTag || context.ref.replace(/^refs\/tags\//, '');
if (!/^v\d/.test(tag)) {
throw new Error(
`Refusing to release from '${tag}'. Push a v* tag or pass a tag input.`,
);
}
const notes = (context.payload.inputs?.notes || '').trim()
|| 'Download the installer for your platform below.';
const { data } = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tag,
name: `Markappoly ${tag}`,
body: 'Download the installer for your platform below.',
body: notes,
draft: true,
prerelease: false,
});
Expand All @@ -54,6 +71,8 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.tag || github.ref }}

- name: Install Linux dependencies
if: matrix.platform == 'ubuntu-22.04'
Expand Down
13 changes: 13 additions & 0 deletions DISTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ git push origin v0.2.0

The workflow builds every platform, creates a **draft** GitHub Release with the installers attached, and (when the signing key secret is set) attaches the updater `latest.json`. Review the draft and publish it.

You can also dispatch the workflow by tag (Actions → Release → Run workflow) and optionally paste release notes. The daily security job uses that path when it remediates an advisory.

**Required repo secrets** (Settings → Secrets and variables → Actions):

| Secret | Needed for | Status |
Expand All @@ -100,3 +102,14 @@ The workflow builds every platform, creates a **draft** GitHub Release with the
| `APPLE_API_ISSUER` / `APPLE_API_KEY` / `APPLE_API_KEY_BASE64` | macOS notarization (App Store Connect API key) | ✅ set |

macOS builds are signed with the Developer ID certificate and notarized via the App Store Connect API key. The workflow decodes `APPLE_API_KEY_BASE64` into a `.p8` on the runner and points `APPLE_API_KEY_PATH` at it, so downloaded `.dmg`s open with a normal double-click — no Gatekeeper warning. The signing `.p12` was exported from Keychain and stored only as the encrypted `APPLE_CERTIFICATE` secret.

## 6. Daily security audit

`.github/workflows/daily-security.yml` runs every day at 06:00 UTC (and on demand).

1. `npm audit` (high/critical, production deps, honouring `audit-ci.jsonc`) and `cargo audit`.
2. If a non-breaking fix exists: `npm audit fix` / `cargo audit fix`, bump the patch version in `package.json`, `Cargo.toml`, and `tauri.conf.json`.
3. After unit tests and a frontend build pass, it commits, tags `vX.Y.Z`, and dispatches the Release workflow above so a signed draft appears on GitHub Releases.
4. If something high/critical remains and cannot be auto-fixed, it opens (or comments on) an **Outstanding dependency vulnerabilities** issue instead of shipping.

It will not cut a release for allowlisted, still-unpatched advisories. The job needs permission to push to `main` (or a bypass for the Actions bot if the branch is protected).
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ npm run tauri dev # run the app with hot reload
npm run tauri build # produce a native installer for your OS
```

**[DISTRIBUTION.md](DISTRIBUTION.md)** covers signing, notarization, the app icon, and auto-updates.
**[DISTRIBUTION.md](DISTRIBUTION.md)** covers signing, notarization, the app icon, auto-updates, and the daily security-audit release. **[SECURITY.md](SECURITY.md)** is the vulnerability-reporting policy.

**Stack:** React + TypeScript + Vite · Tauri v2 (Rust) · unified/remark/rehype · CodeMirror 6 · KaTeX · Mermaid · `remark-docx`

Expand Down
6 changes: 4 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ Markappoly is distributed through GitHub Releases, and security fixes land on th

| Version | Supported |
| ------- | --------- |
| 0.4.x | ✅ |
| < 0.4 | ❌ |
| Latest release (0.7.x) | ✅ |
| Older releases | ❌ |

A scheduled GitHub Action runs the npm and Rust dependency audits every day. If a high or critical advisory has a non-breaking fix, the job applies it, bumps the patch version, and opens a **draft** GitHub Release through the usual signed-build workflow. Findings with no upstream patch (see `audit-ci.jsonc`) are left on the allowlist and do not cut a release.

## Reporting a vulnerability

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"preview": "vite preview",
"tauri": "tauri",
"test": "vitest run",
"test:watch": "vitest"
"test:watch": "vitest",
"audit": "npx --yes audit-ci@^7 --config audit-ci.jsonc --show-found"
},
"dependencies": {
"@codemirror/autocomplete": "^6.19.0",
Expand Down
Loading
Loading