From 2fdf985ac34a4f0bf32ecd84539b608a9d5dbb9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:43:59 +0800 Subject: [PATCH] ci: add free security baseline (#26) --- LOCAL-DATA-BOUNDARY.md | 69 +++++++++++++++ SECURITY-BASELINE.md | 57 ++++++++++++ scripts/bytefolk-local-boundary-check.rb | 86 +++++++++++++++++++ scripts/bytefolk-scrub-env.sh | 25 ++++++ workflow-templates/bytefolk-scorecard.yml | 49 +++++++++++ .../bytefolk-security.properties.json | 10 +++ workflow-templates/bytefolk-security.yml | 68 +++++++++++++++ 7 files changed, 364 insertions(+) create mode 100644 LOCAL-DATA-BOUNDARY.md create mode 100644 SECURITY-BASELINE.md create mode 100755 scripts/bytefolk-local-boundary-check.rb create mode 100755 scripts/bytefolk-scrub-env.sh create mode 100644 workflow-templates/bytefolk-scorecard.yml create mode 100644 workflow-templates/bytefolk-security.properties.json create mode 100644 workflow-templates/bytefolk-security.yml diff --git a/LOCAL-DATA-BOUNDARY.md b/LOCAL-DATA-BOUNDARY.md new file mode 100644 index 0000000..edc193c --- /dev/null +++ b/LOCAL-DATA-BOUNDARY.md @@ -0,0 +1,69 @@ +# Local Data Boundary + +This procedure protects local-only files from being accidentally included in +repository scans or sent to a remote model, CI service, issue, pull request, +or artifact store. + +## What is protected + +The following must remain outside public repositories and scanner inputs: + +- Alibaba internal source, documents, exports, logs, screenshots, and + credentials; +- `.env` files, private keys, certificates, cloud credentials, and local + provider configuration; +- browser profiles, chat exports, SSH configuration, and unrelated worktrees; +- generated runtime data, databases, caches, and local agent state. + +## Safe local procedure + +From the repository that is actually being inspected, run: + +```bash +scripts/bytefolk-scrub-env.sh ruby scripts/bytefolk-local-boundary-check.rb . +``` + +The check is local-only. The wrapper uses an environment allowlist so GitHub, +cloud, package, and model credentials are not inherited by the validator. The +validator uses Git metadata and local `git grep`, prints counts rather than +matching values, and exits non-zero when it finds a sensitive filename, a +tracked credential-shaped value, or a tracked symlink. It does not call a +network service, an LLM, or GitHub. + +Use an explicit repository path. Never pass the workspace parent, `$HOME`, +the filesystem root, or a directory containing multiple repositories to a +scanner. In particular, do not run `strix --target .` from the ByteFolk +workspace root. + +For a clean, committed-tree-only inspection, first pass the boundary check, +then create a disposable checkout from the exact commit under review. Do not +copy `.env*`, credentials, keys, certificates, runtime state, or unrelated +files into that checkout. A local change that has not been committed should +be reviewed with a local diff or a local scanner; it should not be uploaded to +an external service by default. + +## Remote boundary + +- A GitHub-hosted job sees repository content only after it is pushed or + otherwise supplied to the job. It cannot read files sitting on a developer's + computer. +- Secrets are not safe to expose to arbitrary build, test, or AI steps. Keep + them in the smallest possible job, and do not place them in command-line + arguments, logs, artifacts, issue text, or pull-request comments. +- Do not use `pull_request_target` to check out or execute an untrusted pull + request. Fork pull requests must not receive model-provider or deployment + credentials. +- If an AI or cloud scanner is used, set its telemetry and data-sharing + options explicitly, use an approved endpoint, and record the data boundary + in the validation ledger. +- Do not assume that an environment variable is hidden because it is masked in + CI logs. A local child process can still inherit it. Run deterministic local + validators through `scripts/bytefolk-scrub-env.sh`, and never run arbitrary + repository scripts with a GitHub or provider credential in the environment. + +## If exposure is suspected + +Stop the scan or upload, preserve only redacted evidence, revoke and rotate +the affected credential, and report the incident privately through the +affected repository's Security tab. Removing a leaked file from the latest +commit is not sufficient. diff --git a/SECURITY-BASELINE.md b/SECURITY-BASELINE.md new file mode 100644 index 0000000..5b6a279 --- /dev/null +++ b/SECURITY-BASELINE.md @@ -0,0 +1,57 @@ +# ByteFolk Security Baseline + +This is the organization baseline for public ByteFolk repositories. It is a +defense-in-depth control set; a green check is evidence about the checked +commit and scope, not a guarantee that the application is secure. + +## Adoption order + +1. Enable GitHub Secret Scanning and Push Protection for every public + repository. Restrict bypass to a small maintainer group and rotate any + credential that was exposed, even when the alert is later dismissed. +2. Enable Dependabot alerts and security updates. Configure version updates + for the repository's package managers and for GitHub Actions. +3. Add the `ByteFolk Security Baseline` workflow template. Set the CodeQL + language matrix to the languages actually present in the repository. +4. Add the `ByteFolk Scorecard` workflow template on the default branch and a + scheduled run. +5. Require the security checks, the repository CI, the linked issue, and the + applicable CODEOWNER approval in the repository ruleset. +6. Add artifact attestations to release workflows and verify them before + publishing or consuming release assets. + +## Non-negotiable workflow rules + +- Pin every third-party Action to a full 40-character commit SHA. Keep the + human-readable release tag in a same-line comment so Dependabot can update + it. +- Set `permissions: contents: read` at workflow scope and grant additional + permissions only to the individual job that needs them. +- Do not use `pull_request_target` to check out or execute pull-request code. +- Do not expose release, cloud, package-publishing, OIDC, or model-provider + credentials to tests or scanners that execute repository code. +- Do not use mutable Action refs, Docker `latest` tags, remote install pipes, + or unreviewed downloaded binaries in a security gate. +- Optional third-party workflow linters such as zizmor must be reviewed + separately; they are not part of the blocking baseline until their complete + download and execution chain is approved. +- Treat a scanner execution error, incomplete result, or missing artifact as a + failed gate. Do not convert it into a warning silently. + +## Scope boundary + +GitHub Actions run on a GitHub-hosted runner and receive only the repository +contents checked out by the workflow plus explicitly supplied variables and +secrets. They do not have access to the maintainer's local home directory. +Local scanners are a separate trust boundary. Before using one, follow +[`LOCAL-DATA-BOUNDARY.md`](LOCAL-DATA-BOUNDARY.md) and run the local boundary +check from inside the intended repository. + +## AI and dynamic scanners + +AI-driven or actively probing tools such as Strix are optional additions, not +part of this baseline. If one is adopted later, run it in an isolated, +ephemeral staging environment with synthetic data, no release credentials, +an approved model endpoint, telemetry disabled where policy requires it, and +an explicit egress allowlist. It must not run from the workspace root or from +a directory containing unrelated local repositories. diff --git a/scripts/bytefolk-local-boundary-check.rb b/scripts/bytefolk-local-boundary-check.rb new file mode 100755 index 0000000..ebb8ab5 --- /dev/null +++ b/scripts/bytefolk-local-boundary-check.rb @@ -0,0 +1,86 @@ +#!/usr/bin/env ruby + +require "open3" +require "set" + +GENERATED_PATH_SEGMENTS = %w[node_modules .venv venv dist build .next .cache coverage] + +def run_git!(repo, *args) + stdout, _stderr, status = Open3.capture3("git", "-C", repo, *args) + return stdout if status.success? + + abort "local boundary check could not inspect the repository (git #{args.first} failed)" +end + +def sensitive_path?(path) + normalized = path.downcase + segments = normalized.split("/") + basename = File.basename(normalized) + + return false if segments.any? { |segment| GENERATED_PATH_SEGMENTS.include?(segment) } + return false if %w[.env.example .env.sample].include?(basename) + + normalized.match?(%r{(^|/)(credentials?|secrets?|private|id_rsa|id_ed25519)(/|$)}) || + segments.any? { |segment| segment.start_with?(".env") && !segment.end_with?(".example") && !segment.end_with?(".sample") } || + %w[.npmrc .pypirc .netrc].include?(basename) || + normalized.match?(%r{\.(pem|key|p12|pfx|jks|keystore|tfstate|tfvars)(\.|$)}) +end + +abort "usage: ruby scripts/bytefolk-local-boundary-check.rb REPOSITORY" unless ARGV.length == 1 + +begin + repo = File.realpath(ARGV.fetch(0)) +rescue SystemCallError + abort "local boundary check: repository path does not exist" +end + +root = run_git!(repo, "rev-parse", "--show-toplevel").strip +root = File.realpath(root) +abort "local boundary check: pass one repository, not its parent" unless root == repo +abort "local boundary check: refusing the filesystem root" if root == "/" + +tracked = run_git!(root, "ls-files", "-z").split("\0").reject(&:empty?) +untracked = run_git!(root, "ls-files", "--others", "--exclude-standard", "-z").split("\0").reject(&:empty?) +ignored = run_git!(root, "ls-files", "--others", "--ignored", "--exclude-standard", "-z").split("\0").reject(&:empty?) + +tracked_sensitive = tracked.count { |path| sensitive_path?(path) } +untracked_sensitive = (untracked + ignored).count { |path| sensitive_path?(path) } + +symlink_count = run_git!(root, "ls-files", "-s", "-z").split("\0").count do |record| + record.start_with?("120000 ") +end + +suspicious_patterns = [ + "-----BEGIN (RSA|EC|OPENSSH|DSA|PGP) PRIVATE KEY-----", + "\\b(AKIA|ASIA)[0-9A-Z]{16}\\b", + "\\b(LTAI|AKID)[A-Za-z0-9]{12,}\\b", + "\\bgh[pousr]_[A-Za-z0-9_]{20,}\\b", + "\\bgithub_pat_[A-Za-z0-9_]{20,}\\b", + "\\bxox[baprs]-[A-Za-z0-9-]{20,}\\b" +] + +suspicious_files = Set.new +suspicious_patterns.each do |pattern| + stdout, _stderr, status = Open3.capture3( + "git", "-C", root, "grep", "-I", "-i", "-l", "-E", pattern, "--" + ) + suspicious_files.merge(stdout.split("\n")) if status.success? +end + +puts "repository_files=#{tracked.length}" +puts "untracked_files=#{untracked.length}" +puts "ignored_files=#{ignored.length}" +puts "tracked_sensitive_names=#{tracked_sensitive}" +puts "untracked_or_ignored_sensitive_names=#{untracked_sensitive}" +puts "tracked_symlinks=#{symlink_count}" +puts "suspicious_content_files=#{suspicious_files.length}" + +violations = tracked_sensitive + untracked_sensitive + symlink_count + suspicious_files.length +if violations.zero? + puts "result=PASS" + exit 0 +end + +puts "result=HOLD" +puts "details=suppressed; inspect locally without copying values into logs" +exit 1 diff --git a/scripts/bytefolk-scrub-env.sh b/scripts/bytefolk-scrub-env.sh new file mode 100755 index 0000000..7801d07 --- /dev/null +++ b/scripts/bytefolk-scrub-env.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ "$#" -eq 0 ]]; then + echo "usage: scripts/bytefolk-scrub-env.sh COMMAND [ARG ...]" >&2 + exit 2 +fi + +# Use an allowlist instead of trying to guess every provider-specific secret +# variable. This wrapper is for local scanners and validators that do not need +# credentials. It does not grant filesystem isolation; keep the command inside +# one explicit repository and run the boundary check first. +safe_path="${PATH:-/usr/bin:/bin:/usr/sbin:/sbin}" +safe_lang="${LANG:-C}" +safe_lc_all="${LC_ALL:-C}" +safe_tmpdir="${TMPDIR:-/tmp}" + +exec env -i \ + PATH="$safe_path" \ + LANG="$safe_lang" \ + LC_ALL="$safe_lc_all" \ + TMPDIR="$safe_tmpdir" \ + GIT_TERMINAL_PROMPT=0 \ + "$@" diff --git a/workflow-templates/bytefolk-scorecard.yml b/workflow-templates/bytefolk-scorecard.yml new file mode 100644 index 0000000..88307ad --- /dev/null +++ b/workflow-templates/bytefolk-scorecard.yml @@ -0,0 +1,49 @@ +name: ByteFolk Scorecard + +on: + push: + branches: + - main + schedule: + - cron: "43 3 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + scorecard: + name: OpenSSF Scorecard + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + actions: read + pull-requests: read + security-events: write + id-token: write + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run Scorecard analysis + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + - name: Upload Scorecard artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: scorecard-results + path: results.sarif + if-no-files-found: error + retention-days: 14 + + - name: Upload Scorecard results + uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.4 + with: + sarif_file: results.sarif diff --git a/workflow-templates/bytefolk-security.properties.json b/workflow-templates/bytefolk-security.properties.json new file mode 100644 index 0000000..5cfff73 --- /dev/null +++ b/workflow-templates/bytefolk-security.properties.json @@ -0,0 +1,10 @@ +{ + "name": "ByteFolk Security Baseline", + "description": "Pinned CodeQL and dependency-review checks for ByteFolk repositories", + "iconName": "shield", + "categories": [ + "Code scanning", + "Dependency management", + "Security" + ] +} diff --git a/workflow-templates/bytefolk-security.yml b/workflow-templates/bytefolk-security.yml new file mode 100644 index 0000000..3b69a2b --- /dev/null +++ b/workflow-templates/bytefolk-security.yml @@ -0,0 +1,68 @@ +name: ByteFolk Security Baseline + +on: + pull_request: + push: + branches: + - main + schedule: + - cron: "17 3 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + dependency-review: + name: Dependency review + if: ${{ github.event_name == 'pull_request' }} + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Review dependency changes + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + fail-on-severity: high + fail-on-scopes: runtime + comment-summary-in-pr: never + + codeql: + name: CodeQL (${{ matrix.language }}) + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + actions: read + packages: read + security-events: write + strategy: + fail-fast: false + matrix: + # Change this list to the languages present in the repository. + language: + - javascript-typescript + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Initialize CodeQL + uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.4 + with: + languages: ${{ matrix.language }} + queries: security-extended + + - name: Autobuild + uses: github/codeql-action/autobuild@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.4 + + - name: Analyze + uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.4 + with: + category: /language:${{ matrix.language }}