Repository navigation
205 lines (196 loc) · 10.7 KB
/
Copy pathsecurity.yml
File metadata and controls
205 lines (196 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
name: Security
on:
pull_request:
push:
branches: [main]
schedule:
- cron: "17 6 * * 1" # Monday morning: catch CVEs disclosed since the last push
# Cancel outdated pull request runs; on main, run per commit.
concurrency:
group: security-${{ github.event_name == 'pull_request' && github.ref || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
# Read-only by default. Jobs that need more ask for it explicitly.
permissions: read-all
jobs:
secrets:
name: secret scan
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # gitleaks needs history to scan every commit in the PR
- name: gitleaks (diff)
uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0
env:
GITHUB_TOKEN: ${{ github.token }}
# The action above only scans the commits in this push or PR. That misses a secret that
# entered on an earlier commit and is still sitting in the tree -- exactly how a CLI
# transcript quoting a PyPI token once survived in main. This second pass
# scans the checked-out files themselves, so anything present in HEAD is caught no matter
# which commit put it there.
- name: gitleaks (working tree)
run: |
VER=8.30.1
# Digest of gitleaks_${VER}_linux_x64.tar.gz from the v${VER} release,
# cross-checked against gitleaks_${VER}_checksums.txt in the same release.
SHA256=551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb
curl -sSL -o gitleaks.tar.gz \
"https://github.com/gitleaks/gitleaks/releases/download/v${VER}/gitleaks_${VER}_linux_x64.tar.gz"
echo "${SHA256} gitleaks.tar.gz" | sha256sum -c
tar -xzf gitleaks.tar.gz gitleaks
./gitleaks dir . --no-banner --redact --exit-code 1
deps:
name: dependency CVEs
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"
- run: pip install pip-audit
- name: Audit shipped dependencies (core + [serve], blocking)
run: |
# Audit what the package DECLARES, not a local install. Installing torch from the
# PyTorch CPU index yields a local version (2.14.0+cpu) that does not exist on PyPI,
# so pip-audit cannot resolve it and --strict fails the job on that alone.
#
# The blocking scope is what the package and the image actually ship: [project.dependencies]
# plus the [serve] extra -- the Dockerfile ends with `pip install ".[serve]"`, so fastapi,
# uvicorn and python-multipart and their transitive closure run inside the published image,
# and an advisory in any of them has to fail every PR. Read the tables rather than
# transcribing a list, so an extra added later is audited by the commit that adds it.
#
# The remaining extras (langchain, crewai, llamaindex, ...) are audited separately, see
# the next step: a finding there is still reported, but it does not fail unrelated PRs.
python - <<'PY' > requirements-audit-core.txt
import tomllib
with open("pyproject.toml", "rb") as fh:
project = tomllib.load(fh)["project"]
specs = list(project.get("dependencies", []))
specs.extend(project.get("optional-dependencies", {}).get("serve", []))
seen = set()
for spec in specs:
if spec not in seen:
seen.add(spec)
print(spec)
PY
cat requirements-audit-core.txt
echo "strictly auditing $(wc -l < requirements-audit-core.txt) shipped names: the core plus [serve]"
pip-audit --strict --desc -r requirements-audit-core.txt
- name: Audit optional-integration dependencies (findings advisory, failures blocking)
# The integration extras pull transitives that ship nowhere: no laya surface runs a Chroma
# server or calls nltk's model save/load, and crewai pins json-repair below the patched
# release (#645). Those advisories still deserve eyes, so they are reported here as an
# annotation, but they must not turn every PR red -- a gate that is always red teaches
# everyone to skim past it, including the day one of these packages ships a fix (#646).
#
# Only FINDINGS are advisory. pip-audit exits non-zero both for findings and for a broken
# audit -- a specifier it cannot resolve, a resolver or tool crash -- and its report always
# carries the findings count. So a failure WITH a report is advisory; a failure WITHOUT one
# is the audit itself breaking, and that still fails the job exactly as it did before.
run: |
python - <<'PY' > requirements-audit-extras.txt
import tomllib
with open("pyproject.toml", "rb") as fh:
project = tomllib.load(fh)["project"]
seen = set()
for extra, specs in project.get("optional-dependencies", {}).items():
if extra == "serve":
continue # already in the blocking audit above
for spec in specs:
if spec not in seen: # `langchain` and `langgraph` repeat two of these
seen.add(spec)
print(spec)
PY
cat requirements-audit-extras.txt
echo "advisorially auditing $(wc -l < requirements-audit-extras.txt) optional-integration names"
# `set -e` is on: capture the exit code without tripping it, then decide below.
pip-audit --strict --desc -r requirements-audit-extras.txt > pip-audit-extras.log 2>&1 && code=0 || code=$?
cat pip-audit-extras.log
if [ "$code" -ne 0 ] && ! grep -q "known vulnerabilities" pip-audit-extras.log; then
echo "::error::the extras audit itself failed (exit $code) -- resolver or tooling error, still blocking"
exit "$code"
fi
if [ "$code" -ne 0 ]; then
# Name the advisories in the annotation itself, so a triager sees WHAT was found
# without opening the log (#694 review). Only the pip-audit table IDs (GHSA/PYSEC):
# --desc bodies quote CVE- ids in prose, and a warning that repeats prose noise is
# a warning nobody reads.
ids=$(grep -oE "(GHSA|PYSEC)-[A-Za-z0-9-]+" pip-audit-extras.log | sort -u | tr '\n' ' ')
echo "::warning::advisories in optional-integration dependencies: ${ids% } -- visible here, not blocking PRs (see #645)"
fi
# Java as well as Python. This job named `python` only, so `laya-java` -- ~35,000 lines that
# compile to a jar and publish to Maven Central -- had no taint analysis over it at all.
# CodeQL is the only free engine within reach that does real interprocedural dataflow, which is
# what finds a sink several calls away from its source; the pattern scanners in `java.yml`
# cannot see that.
#
# A matrix rather than `languages: python, java-kotlin` in one cell: the Java cell needs a JDK
# and a build, and keeping them separate means a Java build failure cannot take the Python
# analysis down with it.
codeql:
name: CodeQL (${{ matrix.language }})
runs-on: ubuntu-latest
timeout-minutes: 25
permissions:
security-events: write
actions: read
contents: read
strategy:
fail-fast: false
matrix:
# `javascript-typescript` needs no build step, so adding the TypeScript SDK costs one
# matrix cell and no build risk. `csharp` is NOT here: CodeQL would have to run
# `dotnet build` for it, and a build this change cannot verify locally would turn a
# scanner into a broken required check. laya-dotnet is covered by gitleaks only today;
# adding it needs whoever owns that build to confirm autobuild works.
language: [python, java-kotlin, javascript-typescript]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Only the Java cell compiles anything, so only it needs a JDK -- and the wrapper jar it
# would execute is validated first, for the reason `java.yml` gives: it is a binary that
# runs at build time and is the one file here a reviewer cannot read.
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
if: ${{ matrix.language == 'java-kotlin' }}
with:
distribution: temurin
java-version: "17"
cache: gradle
- uses: gradle/actions/wrapper-validation@0b6dd653ba04f4f93bf581ec31e66cbd7dcb644d # v4
if: ${{ matrix.language == 'java-kotlin' }}
- uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
languages: ${{ matrix.language }}
queries: security-extended
# Python needs no build step; Java does, and autobuild finds the Gradle build itself.
- uses: github/codeql-action/autobuild@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
if: ${{ matrix.language == 'java-kotlin' }}
- uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
supply_chain:
name: no unsafe deserialization
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Weights must load through safetensors, never pickle
run: |
# torch.load / pickle execute arbitrary code during deserialization. Laya loads
# checkpoints from the Hub, so a pickle path would be remote code execution on
# `laya.load("someone/their-model")`.
if grep -rnE --include='*.py' "\bpickle\b|torch\.load\(|joblib\.load\(|yaml\.load\(" laya/ laya-ts/scripts/; then
echo "::error::unsafe deserialization in the shipped package - use safetensors"
exit 1
fi
echo "OK: no unsafe deserialization in laya/ or laya-ts/scripts/"
- name: No shell-out or dynamic exec in the shipped package
run: |
if grep -rnE "os\.system\(|subprocess\.|shell=True|\beval\(|\bexec\(" laya/ \
| grep -v "\.eval()"; then
echo "::error::shell-out or dynamic execution in the shipped package"
exit 1
fi
echo "OK: no os.system/subprocess/eval/exec in laya/"
- name: ONNX exporter requires safetensors weights
run: python tests/test_export_onnx_safety.py