saferun-cli 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. saferun_cli-0.0.2/.github/workflows/ci.yml +224 -0
  2. saferun_cli-0.0.2/.github/workflows/release-analyzer.yml +208 -0
  3. saferun_cli-0.0.2/IMPLEMENTATION.md +463 -0
  4. saferun_cli-0.0.2/LICENSE +21 -0
  5. saferun_cli-0.0.2/MANIFEST.in +11 -0
  6. saferun_cli-0.0.2/PKG-INFO +136 -0
  7. saferun_cli-0.0.2/PROJECT.md +267 -0
  8. saferun_cli-0.0.2/README.md +104 -0
  9. saferun_cli-0.0.2/SECURITY.md +21 -0
  10. saferun_cli-0.0.2/docker/analyzer/Dockerfile +43 -0
  11. saferun_cli-0.0.2/docker/analyzer/README.md +20 -0
  12. saferun_cli-0.0.2/docker/analyzer/THIRD_PARTY_NOTICES.md +125 -0
  13. saferun_cli-0.0.2/docker/analyzer/build.py +62 -0
  14. saferun_cli-0.0.2/docker/analyzer/build.sh +5 -0
  15. saferun_cli-0.0.2/docker/analyzer/fetch_verified.py +64 -0
  16. saferun_cli-0.0.2/docker/analyzer/licenses/osv-scanner-v2.7.0-LICENSE +202 -0
  17. saferun_cli-0.0.2/docker/analyzer/licenses/osv-scanner-v2.7.0-go-module-licenses.txt +19967 -0
  18. saferun_cli-0.0.2/docker/analyzer/licenses/osv-scanner-v2.7.0-go-modules.md +199 -0
  19. saferun_cli-0.0.2/docker/analyzer/licenses/semgrep-LICENSE +504 -0
  20. saferun_cli-0.0.2/docker/analyzer/semgrep-requirements.txt +69 -0
  21. saferun_cli-0.0.2/docs/cli.md +142 -0
  22. saferun_cli-0.0.2/docs/engine.md +30 -0
  23. saferun_cli-0.0.2/docs/install-and-test.md +72 -0
  24. saferun_cli-0.0.2/docs/platform-test-checklist.md +14 -0
  25. saferun_cli-0.0.2/docs/platforms.md +90 -0
  26. saferun_cli-0.0.2/docs/release-checklist-analyzer-v1.0.1.md +104 -0
  27. saferun_cli-0.0.2/docs/release-images.md +104 -0
  28. saferun_cli-0.0.2/docs/release-notes-analyzer-v1.0.1.md +36 -0
  29. saferun_cli-0.0.2/docs/release-pypi.md +57 -0
  30. saferun_cli-0.0.2/docs/sandbox.md +24 -0
  31. saferun_cli-0.0.2/pyproject.toml +56 -0
  32. saferun_cli-0.0.2/saferun/__init__.py +3 -0
  33. saferun_cli-0.0.2/saferun/__main__.py +5 -0
  34. saferun_cli-0.0.2/saferun/cli.py +533 -0
  35. saferun_cli-0.0.2/saferun/dockerdiag.py +65 -0
  36. saferun_cli-0.0.2/saferun/doctor.py +159 -0
  37. saferun_cli-0.0.2/saferun/engine/__init__.py +7 -0
  38. saferun_cli-0.0.2/saferun/engine/analyzer.py +30 -0
  39. saferun_cli-0.0.2/saferun/engine/context.py +27 -0
  40. saferun_cli-0.0.2/saferun/engine/engine.py +100 -0
  41. saferun_cli-0.0.2/saferun/engine/model.py +146 -0
  42. saferun_cli-0.0.2/saferun/engine/node.py +145 -0
  43. saferun_cli-0.0.2/saferun/engine/osv.py +291 -0
  44. saferun_cli-0.0.2/saferun/engine/osvdb.py +367 -0
  45. saferun_cli-0.0.2/saferun/engine/paths.py +41 -0
  46. saferun_cli-0.0.2/saferun/engine/pysyntax.py +135 -0
  47. saferun_cli-0.0.2/saferun/engine/rules/python.yml +103 -0
  48. saferun_cli-0.0.2/saferun/engine/semgrep.py +173 -0
  49. saferun_cli-0.0.2/saferun/engine/tools.py +167 -0
  50. saferun_cli-0.0.2/saferun/engine/verdict.py +47 -0
  51. saferun_cli-0.0.2/saferun/hostos/__init__.py +188 -0
  52. saferun_cli-0.0.2/saferun/hostos/_win.py +316 -0
  53. saferun_cli-0.0.2/saferun/hostos/acl.py +172 -0
  54. saferun_cli-0.0.2/saferun/hostos/fsops.py +41 -0
  55. saferun_cli-0.0.2/saferun/hostos/process.py +82 -0
  56. saferun_cli-0.0.2/saferun/hostos/winfs.py +164 -0
  57. saferun_cli-0.0.2/saferun/runcmd.py +139 -0
  58. saferun_cli-0.0.2/saferun/sandbox/__init__.py +11 -0
  59. saferun_cli-0.0.2/saferun/sandbox/analyzer.py +192 -0
  60. saferun_cli-0.0.2/saferun/sandbox/attestation.py +110 -0
  61. saferun_cli-0.0.2/saferun/sandbox/config.py +121 -0
  62. saferun_cli-0.0.2/saferun/sandbox/controls.py +104 -0
  63. saferun_cli-0.0.2/saferun/sandbox/copy.py +338 -0
  64. saferun_cli-0.0.2/saferun/sandbox/copy_windows.py +281 -0
  65. saferun_cli-0.0.2/saferun/sandbox/cosign_tool.py +134 -0
  66. saferun_cli-0.0.2/saferun/sandbox/docker.py +167 -0
  67. saferun_cli-0.0.2/saferun/sandbox/images.py +110 -0
  68. saferun_cli-0.0.2/saferun/sandbox/profiles.py +40 -0
  69. saferun_cli-0.0.2/saferun/sandbox/reaper.py +127 -0
  70. saferun_cli-0.0.2/saferun/sandbox/runner.py +268 -0
  71. saferun_cli-0.0.2/saferun/sandbox/secrets.py +91 -0
  72. saferun_cli-0.0.2/saferun/sandbox/signature.py +117 -0
  73. saferun_cli-0.0.2/saferun/sandbox/verify.py +75 -0
  74. saferun_cli-0.0.2/saferun/sandbox/workspace.py +92 -0
  75. saferun_cli-0.0.2/saferun/setup.py +559 -0
  76. saferun_cli-0.0.2/saferun/ui.py +133 -0
  77. saferun_cli-0.0.2/saferun_cli.egg-info/PKG-INFO +136 -0
  78. saferun_cli-0.0.2/saferun_cli.egg-info/SOURCES.txt +190 -0
  79. saferun_cli-0.0.2/saferun_cli.egg-info/dependency_links.txt +1 -0
  80. saferun_cli-0.0.2/saferun_cli.egg-info/entry_points.txt +2 -0
  81. saferun_cli-0.0.2/saferun_cli.egg-info/requires.txt +6 -0
  82. saferun_cli-0.0.2/saferun_cli.egg-info/top_level.txt +1 -0
  83. saferun_cli-0.0.2/setup.cfg +4 -0
  84. saferun_cli-0.0.2/tests/conftest.py +106 -0
  85. saferun_cli-0.0.2/tests/engine_helpers.py +134 -0
  86. saferun_cli-0.0.2/tests/posix_fsops.py +85 -0
  87. saferun_cli-0.0.2/tests/test_adversarial.py +410 -0
  88. saferun_cli-0.0.2/tests/test_analyzer_isolation.py +409 -0
  89. saferun_cli-0.0.2/tests/test_analyzer_lock.py +168 -0
  90. saferun_cli-0.0.2/tests/test_ci_workflow.py +56 -0
  91. saferun_cli-0.0.2/tests/test_cleanup.py +184 -0
  92. saferun_cli-0.0.2/tests/test_cleanup_hardening.py +137 -0
  93. saferun_cli-0.0.2/tests/test_cli.py +191 -0
  94. saferun_cli-0.0.2/tests/test_copy_windows_engine.py +164 -0
  95. saferun_cli-0.0.2/tests/test_engine_integration.py +176 -0
  96. saferun_cli-0.0.2/tests/test_engine_node.py +155 -0
  97. saferun_cli-0.0.2/tests/test_engine_osv.py +257 -0
  98. saferun_cli-0.0.2/tests/test_engine_pysyntax.py +111 -0
  99. saferun_cli-0.0.2/tests/test_engine_real_tools.py +131 -0
  100. saferun_cli-0.0.2/tests/test_engine_semgrep.py +157 -0
  101. saferun_cli-0.0.2/tests/test_engine_tools.py +83 -0
  102. saferun_cli-0.0.2/tests/test_engine_verdict.py +69 -0
  103. saferun_cli-0.0.2/tests/test_hardening.py +496 -0
  104. saferun_cli-0.0.2/tests/test_hardlinks.py +139 -0
  105. saferun_cli-0.0.2/tests/test_hostos_acl.py +167 -0
  106. saferun_cli-0.0.2/tests/test_hostos_platform.py +68 -0
  107. saferun_cli-0.0.2/tests/test_image_pins.py +317 -0
  108. saferun_cli-0.0.2/tests/test_license.py +26 -0
  109. saferun_cli-0.0.2/tests/test_macos_platform.py +34 -0
  110. saferun_cli-0.0.2/tests/test_node_profile.py +118 -0
  111. saferun_cli-0.0.2/tests/test_osv_db_integrity.py +412 -0
  112. saferun_cli-0.0.2/tests/test_races.py +441 -0
  113. saferun_cli-0.0.2/tests/test_release_signature.py +364 -0
  114. saferun_cli-0.0.2/tests/test_report_fixture.py +68 -0
  115. saferun_cli-0.0.2/tests/test_require_docker.py +28 -0
  116. saferun_cli-0.0.2/tests/test_sandbox.py +242 -0
  117. saferun_cli-0.0.2/tests/test_sensitive.py +202 -0
  118. saferun_cli-0.0.2/tests/test_setup.py +675 -0
  119. saferun_cli-0.0.2/tests/test_stage2_install.py +580 -0
  120. saferun_cli-0.0.2/tests/test_ui.py +321 -0
  121. saferun_cli-0.0.2/tests/test_windows_native.py +613 -0
  122. saferun_cli-0.0.2/tests/test_workspace.py +155 -0
  123. saferun_cli-0.0.2/web/.node-version +1 -0
  124. saferun_cli-0.0.2/web/index.html +14 -0
  125. saferun_cli-0.0.2/web/package-lock.json +1864 -0
  126. saferun_cli-0.0.2/web/package.json +25 -0
  127. saferun_cli-0.0.2/web/src/App.jsx +16 -0
  128. saferun_cli-0.0.2/web/src/components/CheckCoverage/CheckCoverage.jsx +36 -0
  129. saferun_cli-0.0.2/web/src/components/CommandBlock/CommandBlock.css +5 -0
  130. saferun_cli-0.0.2/web/src/components/CommandBlock/CommandBlock.jsx +30 -0
  131. saferun_cli-0.0.2/web/src/components/Emblem/Emblem.css +27 -0
  132. saferun_cli-0.0.2/web/src/components/Emblem/Emblem.jsx +45 -0
  133. saferun_cli-0.0.2/web/src/components/FindingsTable/FindingsTable.css +12 -0
  134. saferun_cli-0.0.2/web/src/components/FindingsTable/FindingsTable.jsx +77 -0
  135. saferun_cli-0.0.2/web/src/components/GapsPanel/GapsPanel.css +1 -0
  136. saferun_cli-0.0.2/web/src/components/GapsPanel/GapsPanel.jsx +14 -0
  137. saferun_cli-0.0.2/web/src/components/Mark/Mark.jsx +9 -0
  138. saferun_cli-0.0.2/web/src/components/ReportView/ReportView.jsx +17 -0
  139. saferun_cli-0.0.2/web/src/components/ReportView/ReportView.test.jsx +110 -0
  140. saferun_cli-0.0.2/web/src/components/Section/Section.css +22 -0
  141. saferun_cli-0.0.2/web/src/components/Section/Section.jsx +30 -0
  142. saferun_cli-0.0.2/web/src/components/SeverityTally/SeverityTally.css +1 -0
  143. saferun_cli-0.0.2/web/src/components/SeverityTally/SeverityTally.jsx +13 -0
  144. saferun_cli-0.0.2/web/src/components/SiteFooter/SiteFooter.css +4 -0
  145. saferun_cli-0.0.2/web/src/components/SiteFooter/SiteFooter.jsx +26 -0
  146. saferun_cli-0.0.2/web/src/components/SiteNav/SiteNav.css +19 -0
  147. saferun_cli-0.0.2/web/src/components/SiteNav/SiteNav.jsx +28 -0
  148. saferun_cli-0.0.2/web/src/components/StatusBadge/StatusBadge.css +3 -0
  149. saferun_cli-0.0.2/web/src/components/StatusBadge/StatusBadge.jsx +8 -0
  150. saferun_cli-0.0.2/web/src/components/UpgradePlan/UpgradePlan.css +1 -0
  151. saferun_cli-0.0.2/web/src/components/UpgradePlan/UpgradePlan.jsx +30 -0
  152. saferun_cli-0.0.2/web/src/components/VerdictCard/VerdictCard.css +11 -0
  153. saferun_cli-0.0.2/web/src/components/VerdictCard/VerdictCard.jsx +42 -0
  154. saferun_cli-0.0.2/web/src/components/landing/CheckTicker/CheckTicker.css +10 -0
  155. saferun_cli-0.0.2/web/src/components/landing/CheckTicker/CheckTicker.jsx +18 -0
  156. saferun_cli-0.0.2/web/src/components/landing/Faq/Faq.css +5 -0
  157. saferun_cli-0.0.2/web/src/components/landing/Faq/Faq.jsx +26 -0
  158. saferun_cli-0.0.2/web/src/components/landing/Features/Features.css +5 -0
  159. saferun_cli-0.0.2/web/src/components/landing/Features/Features.jsx +20 -0
  160. saferun_cli-0.0.2/web/src/components/landing/FlowDiagram/FlowDiagram.css +23 -0
  161. saferun_cli-0.0.2/web/src/components/landing/FlowDiagram/FlowDiagram.jsx +33 -0
  162. saferun_cli-0.0.2/web/src/components/landing/GithubCta/GithubCta.css +4 -0
  163. saferun_cli-0.0.2/web/src/components/landing/GithubCta/GithubCta.jsx +21 -0
  164. saferun_cli-0.0.2/web/src/components/landing/HeroProof/HeroProof.css +9 -0
  165. saferun_cli-0.0.2/web/src/components/landing/HeroProof/HeroProof.jsx +23 -0
  166. saferun_cli-0.0.2/web/src/components/landing/HowItWorks/HowItWorks.css +7 -0
  167. saferun_cli-0.0.2/web/src/components/landing/HowItWorks/HowItWorks.jsx +20 -0
  168. saferun_cli-0.0.2/web/src/components/landing/ProblemSolution/ProblemSolution.css +10 -0
  169. saferun_cli-0.0.2/web/src/components/landing/ProblemSolution/ProblemSolution.jsx +28 -0
  170. saferun_cli-0.0.2/web/src/components/landing/QuickStart/QuickStart.css +10 -0
  171. saferun_cli-0.0.2/web/src/components/landing/QuickStart/QuickStart.jsx +20 -0
  172. saferun_cli-0.0.2/web/src/components/landing/ReportDemo/ReportDemo.css +13 -0
  173. saferun_cli-0.0.2/web/src/components/landing/ReportDemo/ReportDemo.jsx +41 -0
  174. saferun_cli-0.0.2/web/src/components/landing/ScrollHero/ScrollHero.css +80 -0
  175. saferun_cli-0.0.2/web/src/components/landing/ScrollHero/ScrollHero.jsx +103 -0
  176. saferun_cli-0.0.2/web/src/components/landing/SecuritySection/SecuritySection.css +8 -0
  177. saferun_cli-0.0.2/web/src/components/landing/SecuritySection/SecuritySection.jsx +20 -0
  178. saferun_cli-0.0.2/web/src/components/landing/TerminalDemo/TerminalDemo.css +29 -0
  179. saferun_cli-0.0.2/web/src/components/landing/TerminalDemo/TerminalDemo.jsx +30 -0
  180. saferun_cli-0.0.2/web/src/content/site.js +53 -0
  181. saferun_cli-0.0.2/web/src/fixtures/report.json +264 -0
  182. saferun_cli-0.0.2/web/src/index.css +46 -0
  183. saferun_cli-0.0.2/web/src/main.jsx +6 -0
  184. saferun_cli-0.0.2/web/src/pages/GuidePage/GuidePage.css +115 -0
  185. saferun_cli-0.0.2/web/src/pages/GuidePage/GuidePage.jsx +392 -0
  186. saferun_cli-0.0.2/web/src/pages/GuidePage/GuidePage.test.jsx +141 -0
  187. saferun_cli-0.0.2/web/src/pages/LandingPage/LandingPage.jsx +34 -0
  188. saferun_cli-0.0.2/web/src/pages/LandingPage/LandingPage.test.jsx +174 -0
  189. saferun_cli-0.0.2/web/src/test-setup.js +4 -0
  190. saferun_cli-0.0.2/web/src/types/index.js +21 -0
  191. saferun_cli-0.0.2/web/src/utils/report.js +97 -0
  192. saferun_cli-0.0.2/web/vite.config.js +10 -0
@@ -0,0 +1,224 @@
1
+ # Continuous integration: tests and checks only. This workflow never publishes anything, never logs in to a registry, has no secrets and only
2
+ # read access to the repository. Publishing the analyzer image is a separate, tag-triggered workflow (release-analyzer.yml).
3
+ #
4
+ # Jobs marked "advisory" use continue-on-error: they report problems without blocking a merge, because they depend on the runner's Docker
5
+ # daemon or on third-party advisory databases. Every action is pinned to a full commit SHA.
6
+ name: ci
7
+
8
+ on:
9
+ pull_request:
10
+ push:
11
+ branches:
12
+ - main
13
+ - 'feature/**'
14
+
15
+ permissions:
16
+ contents: read
17
+
18
+ concurrency:
19
+ group: ci-${{ github.workflow }}-${{ github.ref }}
20
+ cancel-in-progress: true
21
+
22
+ jobs:
23
+ python:
24
+ name: Python tests (Docker tests excluded)
25
+ runs-on: ubuntu-24.04
26
+ timeout-minutes: 25
27
+ steps:
28
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
29
+ with:
30
+ persist-credentials: false
31
+
32
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
33
+ with:
34
+ python-version: "3.12"
35
+ cache: pip
36
+ cache-dependency-path: pyproject.toml
37
+
38
+ - name: Install SafeRun with the dev extra
39
+ run: python -m pip install -e '.[dev]'
40
+
41
+ - name: Syntax check of all Python sources and the shell build script
42
+ run: |
43
+ python -m compileall -q saferun tests docker/analyzer/fetch_verified.py docker/analyzer/build.py
44
+ sh -n docker/analyzer/build.sh
45
+
46
+ - name: Run the tests that do not need Docker
47
+ # Docker-dependent tests are covered by the separate (advisory) docker job. They are deselected here, not hidden: see the header line.
48
+ run: python -m pytest -q -ra -m "not docker"
49
+
50
+ platforms:
51
+ name: Python tests on ${{ matrix.os }} (Docker tests excluded)
52
+ # Native macOS and Windows runs of everything that does not need a Linux Docker daemon. GitHub's macOS and Windows runners cannot run the
53
+ # Linux sandbox containers, so Docker integration is verified on Linux only (the docker job below); see docs/platforms.md.
54
+ # The Intel macOS leg is advisory: GitHub may retire that runner image.
55
+ runs-on: ${{ matrix.os }}
56
+ timeout-minutes: 30
57
+ continue-on-error: ${{ matrix.experimental }}
58
+ strategy:
59
+ fail-fast: false
60
+ matrix:
61
+ include:
62
+ - os: windows-2022
63
+ experimental: false
64
+ - os: macos-15
65
+ experimental: false
66
+ - os: macos-15-intel
67
+ experimental: true
68
+ steps:
69
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
70
+ with:
71
+ persist-credentials: false
72
+
73
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
74
+ with:
75
+ python-version: "3.12"
76
+ cache: pip
77
+ cache-dependency-path: pyproject.toml
78
+
79
+ - name: Install SafeRun with the dev extra
80
+ run: python -m pip install -e ".[dev]"
81
+
82
+ - name: Platform and Docker diagnostics (informational)
83
+ shell: bash
84
+ run: |
85
+ python -c "import platform, sys; print(platform.platform(), platform.machine(), sys.version)"
86
+ docker version || echo "docker: not usable on this runner"
87
+ docker info --format 'daemon OSType: {{.OSType}}' || echo "docker daemon: not reachable (expected on this runner)"
88
+
89
+ - name: Run the tests that do not need Docker
90
+ run: python -m pytest -q -ra -m "not docker"
91
+
92
+ - name: State what was not verified
93
+ if: always()
94
+ shell: bash
95
+ run: |
96
+ {
97
+ echo "### ${{ matrix.os }}"
98
+ echo "Docker sandbox and analyzer integration tests were NOT run on this platform (no Linux Docker daemon on the runner)."
99
+ } >> "$GITHUB_STEP_SUMMARY"
100
+
101
+ web:
102
+ name: Website tests and build
103
+ runs-on: ubuntu-24.04
104
+ timeout-minutes: 15
105
+ defaults:
106
+ run:
107
+ working-directory: web
108
+ steps:
109
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
110
+ with:
111
+ persist-credentials: false
112
+
113
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
114
+ with:
115
+ node-version: "22"
116
+ cache: npm
117
+ cache-dependency-path: web/package-lock.json
118
+
119
+ - name: Install from the lockfile without running install scripts
120
+ run: npm ci --ignore-scripts
121
+
122
+ - name: Unit tests
123
+ run: npm test
124
+
125
+ - name: Production build
126
+ run: npm run build
127
+
128
+ package:
129
+ name: Build and inspect the wheel and sdist
130
+ runs-on: ubuntu-24.04
131
+ timeout-minutes: 15
132
+ steps:
133
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
134
+ with:
135
+ persist-credentials: false
136
+
137
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
138
+ with:
139
+ python-version: "3.12"
140
+
141
+ - name: Build
142
+ run: |
143
+ python -m pip install 'build>=1.2' 'twine>=5'
144
+ python -m build
145
+ python -m twine check dist/*
146
+
147
+ - name: Check the contents of both distributions
148
+ run: |
149
+ set -eu
150
+ wheel=$(ls dist/*.whl)
151
+ python -m zipfile -l "$wheel" | grep -q 'saferun/engine/rules/python.yml'
152
+ python -m zipfile -l "$wheel" | grep -q 'saferun/cli.py'
153
+ python -m zipfile -l "$wheel" | grep -q 'saferun/doctor.py'
154
+ for bad in 'docker/' 'tests/' 'web/' 'node_modules' '\.venv'; do
155
+ if python -m zipfile -l "$wheel" | grep -q "$bad"; then echo "wheel contains $bad" >&2; exit 1; fi
156
+ done
157
+ tar tzf dist/*.tar.gz | grep -q 'docker/analyzer/Dockerfile'
158
+ tar tzf dist/*.tar.gz | grep -q 'web/package-lock.json'
159
+ if tar tzf dist/*.tar.gz | grep -q 'node_modules/'; then echo "sdist contains node_modules" >&2; exit 1; fi
160
+
161
+ - name: Install the wheel into a clean virtual environment and run the CLI away from the checkout
162
+ run: |
163
+ set -eu
164
+ python -m venv "$RUNNER_TEMP/clean"
165
+ "$RUNNER_TEMP/clean/bin/pip" install dist/*.whl
166
+ cd "$RUNNER_TEMP"
167
+ "$RUNNER_TEMP/clean/bin/saferun" --help > /dev/null
168
+ "$RUNNER_TEMP/clean/bin/saferun" --version
169
+ # doctor exits 1 on a runner without Docker/analyzer, which is expected; it must still run and print its checks
170
+ "$RUNNER_TEMP/clean/bin/saferun" doctor --format json | python -c "import json,sys; d=json.load(sys.stdin); assert d['checks']"
171
+ "$RUNNER_TEMP/clean/bin/python" -c "import saferun.engine.semgrep as s; assert s.RULES_FILE.is_file(), s.RULES_FILE"
172
+
173
+ audit:
174
+ name: Dependency audit (advisory)
175
+ runs-on: ubuntu-24.04
176
+ timeout-minutes: 15
177
+ continue-on-error: true
178
+ steps:
179
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
180
+ with:
181
+ persist-credentials: false
182
+
183
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
184
+ with:
185
+ node-version: "22"
186
+
187
+ - name: npm audit of the website's production dependencies
188
+ working-directory: web
189
+ run: npm audit --omit=dev --audit-level=high
190
+
191
+
192
+ docker:
193
+ name: Sandbox and analyzer tests with Docker (advisory)
194
+ runs-on: ubuntu-24.04
195
+ timeout-minutes: 45
196
+ continue-on-error: true
197
+ steps:
198
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
199
+ with:
200
+ persist-credentials: false
201
+
202
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
203
+ with:
204
+ python-version: "3.12"
205
+ cache: pip
206
+ cache-dependency-path: pyproject.toml
207
+
208
+ - name: Install SafeRun
209
+ run: python -m pip install -e '.[dev]'
210
+
211
+ - name: Print the runner's Docker and cgroup environment (diagnostic only)
212
+ # Read-only. Helps explain runner-specific differences, e.g. whether Docker reports OOMKilled for a killed container.
213
+ run: |
214
+ docker version
215
+ docker info
216
+ stat -fc %T /sys/fs/cgroup
217
+ uname -r
218
+
219
+ - name: Pull the pinned sandbox images and build the analyzer locally
220
+ # SafeRun pulls by pinned digest and verifies it; the analyzer is built from docker/analyzer (no signed release is used or needed).
221
+ run: saferun init --only python,node,analyzer --build-local
222
+
223
+ - name: Run the Docker-dependent tests
224
+ run: python -m pytest -q -ra -m docker
@@ -0,0 +1,208 @@
1
+ # Builds the SafeRun analyzer image, pushes it to ghcr.io and signs it with cosign (keyless, GitHub OIDC). Phase 5E.
2
+ #
3
+ # Triggered ONLY by pushing a tag `analyzer-v<number>[.<number>...]` whose commit is on main. The tag name is part of the signer
4
+ # identity that `saferun init` demands (saferun/sandbox/images.py: ANALYZER_SIGNER_IDENTITY_REGEXP), so a branch, a pull request or a
5
+ # renamed workflow produces a signature SafeRun refuses. The workflow prints the pushed manifest DIGEST: record it in
6
+ # ANALYZER_RELEASE (saferun/sandbox/images.py) in a reviewed commit; nothing updates the digest automatically.
7
+ #
8
+ # The image is built from the same recipe as docker/analyzer/build.sh (base image by digest, hash-locked Semgrep wheels,
9
+ # checksum-verified OSV-Scanner, rules checked against RULES_SHA256). tests/test_release_signature.py checks this file against images.py.
10
+ #
11
+ # Order, on purpose: build locally (NOT pushed) -> smoke-test that exact image -> log in to ghcr.io -> push that same image -> sign its digest
12
+ # -> verify with SafeRun's identity expression -> check that a wrong identity is refused. Nothing is published unless every earlier step passed,
13
+ # and the registry login happens only after the tests. Signatures made by any other repository or workflow (including the repository this
14
+ # project was copied from) do not satisfy the identity expression and are never trusted.
15
+ name: release-analyzer
16
+
17
+ on:
18
+ push:
19
+ tags: ["analyzer-v*"]
20
+
21
+ permissions:
22
+ contents: read
23
+
24
+ concurrency:
25
+ group: release-analyzer
26
+ cancel-in-progress: false
27
+
28
+ env:
29
+ IMAGE: ghcr.io/vinay-repalle/saferun-analyzer
30
+
31
+ jobs:
32
+ release:
33
+ runs-on: ubuntu-24.04
34
+ timeout-minutes: 40
35
+ permissions:
36
+ contents: read
37
+ packages: write # push to ghcr.io
38
+ id-token: write # keyless cosign signing (GitHub OIDC token)
39
+ steps:
40
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
41
+ with:
42
+ fetch-depth: 0
43
+ persist-credentials: false
44
+
45
+ - name: Check the tag name and that its commit is on main
46
+ env:
47
+ TAG: ${{ github.ref_name }}
48
+ run: |
49
+ set -eu
50
+ printf '%s' "$TAG" | grep -Eq '^analyzer-v[0-9]+(\.[0-9]+)*$' || { echo "tag '$TAG' is not analyzer-v<number>[.<number>...]" >&2; exit 1; }
51
+ git fetch --no-tags origin main
52
+ git merge-base --is-ancestor "$GITHUB_SHA" origin/main || { echo "tag commit is not on main" >&2; exit 1; }
53
+
54
+ - name: Stage the build context (same files as docker/analyzer/build.sh)
55
+ id: ctx
56
+ run: |
57
+ set -eu
58
+ ctx="$RUNNER_TEMP/analyzer-ctx"
59
+ mkdir "$ctx"
60
+ cp docker/analyzer/Dockerfile docker/analyzer/fetch_verified.py docker/analyzer/semgrep-requirements.txt docker/analyzer/THIRD_PARTY_NOTICES.md LICENSE saferun/engine/rules/python.yml "$ctx/"
61
+ cp -r docker/analyzer/licenses "$ctx/licenses"
62
+ echo "path=$ctx" >> "$GITHUB_OUTPUT"
63
+ echo "rules_sha=$(sha256sum "$ctx/python.yml" | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
64
+
65
+ - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
66
+
67
+ - name: Build the image locally (not pushed yet)
68
+ id: build
69
+ uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
70
+ with:
71
+ context: ${{ steps.ctx.outputs.path }}
72
+ platforms: linux/amd64
73
+ load: true
74
+ push: false
75
+ provenance: false
76
+ sbom: false
77
+ build-args: RULES_SHA256=${{ steps.ctx.outputs.rules_sha }}
78
+ tags: ${{ env.IMAGE }}:${{ github.ref_name }}
79
+ labels: |
80
+ org.opencontainers.image.source=https://github.com/${{ github.repository }}
81
+ org.opencontainers.image.revision=${{ github.sha }}
82
+
83
+ - name: Smoke-test the exact image that will be published
84
+ env:
85
+ TAG: ${{ github.ref_name }}
86
+ RULES_SHA: ${{ steps.ctx.outputs.rules_sha }}
87
+ run: |
88
+ set -eu
89
+ img="$IMAGE:$TAG"
90
+ pins=$(python3 -c 'import sys; sys.path.insert(0,"."); from saferun.sandbox import images as i; print(i.SEMGREP_VERSION, i.OSV_SCANNER_VERSION)')
91
+ semgrep_v=${pins%% *}
92
+ osv_v=${pins##* }
93
+ # Same restrictions SafeRun applies when it runs the analyzer: no network, read-only root, no capabilities, non-root.
94
+ run() { docker run --rm --network none --read-only --cap-drop ALL --security-opt no-new-privileges --user 65534:65534 \
95
+ --memory 1g --pids-limit 256 --tmpfs /tmp:rw,noexec,nosuid,size=64m -e HOME=/tmp "$@"; }
96
+ for pair in "saferun.analyzer=1" "saferun.semgrep.version=$semgrep_v" "saferun.osv-scanner.version=$osv_v" "saferun.rules.sha256=$RULES_SHA"; do
97
+ key=${pair%%=*}; want=${pair#*=}
98
+ got=$(docker image inspect --format "{{index .Config.Labels \"$key\"}}" "$img")
99
+ [ "$got" = "$want" ] || { echo "label $key is '$got', expected '$want'" >&2; exit 1; }
100
+ done
101
+ [ "$(docker image inspect --format '{{.Config.User}}' "$img")" = "65534:65534" ] || { echo "the image does not default to the unprivileged user" >&2; exit 1; }
102
+ [ "$(run "$img" semgrep --version)" = "$semgrep_v" ] || { echo "semgrep version mismatch" >&2; exit 1; }
103
+ run "$img" osv-scanner --version | grep -q "osv-scanner version: $osv_v" || { echo "osv-scanner version mismatch" >&2; exit 1; }
104
+ [ "$(run "$img" sha256sum /opt/saferun/rules/python.yml | cut -d' ' -f1)" = "$RULES_SHA" ] || { echo "baked rules differ from the repository rules" >&2; exit 1; }
105
+ # The third-party notices and license texts must be in the image.
106
+ for f in LICENSE THIRD_PARTY_NOTICES.md licenses/semgrep-LICENSE licenses/osv-scanner-v2.7.0-LICENSE licenses/osv-scanner-v2.7.0-go-modules.md licenses/osv-scanner-v2.7.0-go-module-licenses.txt; do
107
+ run "$img" test -s "/usr/share/doc/saferun-analyzer/$f" || { echo "missing in image: $f" >&2; exit 1; }
108
+ done
109
+ # The container must have no network.
110
+ if run "$img" python -c 'import socket; socket.create_connection(("1.1.1.1", 53), 3)' >/dev/null 2>&1; then
111
+ echo "the analyzer container could reach the network" >&2; exit 1
112
+ fi
113
+ # A real (tiny) scan with the baked rules must produce valid JSON that names the scanned file.
114
+ proj=$(mktemp -d); chmod 0755 "$proj"
115
+ printf 'import subprocess\nsubprocess.run("ls", shell=True)\n' > "$proj/sample.py"; chmod 0644 "$proj/sample.py"
116
+ run -v "$proj:/work:ro" "$img" semgrep scan --json --quiet --metrics=off --disable-version-check --no-git-ignore \
117
+ --config /opt/saferun/rules/python.yml /work > "$RUNNER_TEMP/semgrep.json"
118
+ python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); assert "/work/sample.py" in d["paths"]["scanned"], d["paths"]' "$RUNNER_TEMP/semgrep.json"
119
+ echo "smoke test passed for $img"
120
+
121
+ - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
122
+ with:
123
+ registry: ghcr.io
124
+ username: ${{ github.actor }}
125
+ password: ${{ secrets.GITHUB_TOKEN }}
126
+
127
+ - name: Push the tested image and read back its digest
128
+ id: push
129
+ env:
130
+ TAG: ${{ github.ref_name }}
131
+ run: |
132
+ set -eu
133
+ docker push "$IMAGE:$TAG"
134
+ digest=$(docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$IMAGE:$TAG" | grep "^$IMAGE@" | head -n 1 | cut -d@ -f2)
135
+ printf '%s' "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$' || { echo "unexpected digest '$digest'" >&2; exit 1; }
136
+ echo "digest=$digest" >> "$GITHUB_OUTPUT"
137
+
138
+ - uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
139
+ with:
140
+ cosign-release: v2.4.3
141
+
142
+ - name: Sign the pushed digest (keyless)
143
+ env:
144
+ DIGEST: ${{ steps.push.outputs.digest }}
145
+ run: |
146
+ set -eu
147
+ printf '%s' "$DIGEST" | grep -Eq '^sha256:[0-9a-f]{64}$' || { echo "unexpected digest '$DIGEST'" >&2; exit 1; }
148
+ cosign sign --yes "$IMAGE@$DIGEST"
149
+
150
+ - name: Verify with the exact identity expression SafeRun will use
151
+ env:
152
+ DIGEST: ${{ steps.push.outputs.digest }}
153
+ run: |
154
+ set -eu
155
+ regexp=$(python3 -c 'import re,sys; sys.path.insert(0,"."); from saferun.sandbox.images import ANALYZER_SIGNER_IDENTITY_REGEXP as r; print(r)')
156
+ cosign verify \
157
+ --certificate-identity-regexp "$regexp" \
158
+ --certificate-oidc-issuer https://token.actions.githubusercontent.com \
159
+ "$IMAGE@$DIGEST" > /dev/null
160
+
161
+ - name: Check that a wrong signer identity is refused (fails closed)
162
+ env:
163
+ DIGEST: ${{ steps.push.outputs.digest }}
164
+ run: |
165
+ set -u
166
+ if cosign verify \
167
+ --certificate-identity-regexp '^https://github\.com/not-this-owner/not-this-repo/.*$' \
168
+ --certificate-oidc-issuer https://token.actions.githubusercontent.com \
169
+ "$IMAGE@$DIGEST" > /dev/null 2>&1; then
170
+ echo "cosign accepted an identity it must refuse" >&2
171
+ exit 1
172
+ fi
173
+ echo "wrong identity refused, as required"
174
+
175
+ - name: Report an image that was pushed but not fully verified
176
+ if: ${{ always() && steps.push.outcome == 'success' && job.status != 'success' }}
177
+ env:
178
+ TAG: ${{ github.ref_name }}
179
+ run: |
180
+ {
181
+ echo "### WARNING: pushed but NOT signed and verified"
182
+ echo
183
+ echo "A step after the push failed. \`$IMAGE:$TAG\` exists in the registry without a verified signature."
184
+ echo "SafeRun refuses unsigned images. Do NOT record it in ANALYZER_RELEASE. Delete this package version in GitHub (Packages -> saferun-analyzer -> versions), fix the cause, and use a NEW tag."
185
+ } >> "$GITHUB_STEP_SUMMARY"
186
+
187
+ - name: Print what to record in ANALYZER_RELEASE
188
+ env:
189
+ DIGEST: ${{ steps.push.outputs.digest }}
190
+ run: |
191
+ {
192
+ echo "### Analyzer image published and signed"
193
+ echo
194
+ echo "Record this in \`saferun/sandbox/images.py\` in a reviewed commit (rules must be unchanged since this tag):"
195
+ echo
196
+ echo '```python'
197
+ echo "ANALYZER_RELEASE = AnalyzerRelease(ANALYZER_REGISTRY_REPOSITORY, \"$DIGEST\", \"$(date -u +%F)\")"
198
+ echo '```'
199
+ echo
200
+ echo "Reference: \`$IMAGE@$DIGEST\` (tag \`${{ github.ref_name }}\`, commit \`${{ github.sha }}\`)"
201
+ echo "Check from any machine: \`saferun verify-images --signature\` (after recording it)."
202
+ echo
203
+ echo "Manual check without SafeRun (cosign 2.2 or newer):"
204
+ echo '```sh'
205
+ echo "cosign verify --certificate-identity-regexp '^https://github\\.com/${{ github.repository }}/\\.github/workflows/release-analyzer\\.yml@refs/tags/analyzer-v[0-9]+(\\.[0-9]+)*\$' --certificate-oidc-issuer https://token.actions.githubusercontent.com $IMAGE@$DIGEST"
206
+ echo '```'
207
+ echo "The package is private until you make it public once (docs/release-images.md, step 3)."
208
+ } >> "$GITHUB_STEP_SUMMARY"