@@ -221,6 +221,81 @@ def _rule_privileged_sudo(instrs: List[Instruction]) -> Iterable[Finding]:
221221 )
222222
223223
224+ def _rule_relative_workdir (instrs : List [Instruction ]) -> Iterable [Finding ]:
225+ for ins in instrs :
226+ if ins .cmd != "WORKDIR" :
227+ continue
228+ path = ins .args .strip ().strip ('"' ).strip ("'" )
229+ if path and not path .startswith ("/" ) and not path .startswith ("$" ):
230+ yield Finding (
231+ rule_id = "PR008" ,
232+ severity = Severity .LOW ,
233+ line = ins .line ,
234+ message = f"WORKDIR { path !r} is relative — depends on the previous WORKDIR" ,
235+ hint = "Use an absolute path so layer order can't change behaviour." ,
236+ )
237+
238+
239+ def _rule_chmod_777 (instrs : List [Instruction ]) -> Iterable [Finding ]:
240+ pat = re .compile (r"\bchmod\s+(?:-[Rr]\s+)?(?:0?777|a\+rwx)\b" )
241+ for ins in instrs :
242+ if ins .cmd != "RUN" :
243+ continue
244+ if pat .search (ins .args ):
245+ yield Finding (
246+ rule_id = "PR009" ,
247+ severity = Severity .HIGH ,
248+ line = ins .line ,
249+ message = "`chmod 777` (or `a+rwx`) gives world-write permission" ,
250+ hint = "Pick a tighter mode (e.g. 750 for dirs, 640 for files) and chown to the runtime user." ,
251+ )
252+
253+
254+ def _rule_missing_healthcheck (instrs : List [Instruction ]) -> Iterable [Finding ]:
255+ if not any (i .cmd in ("EXPOSE" , "CMD" , "ENTRYPOINT" ) for i in instrs ):
256+ return
257+ if any (i .cmd == "HEALTHCHECK" for i in instrs ):
258+ return
259+ last = instrs [- 1 ] if instrs else None
260+ yield Finding (
261+ rule_id = "PR010" ,
262+ severity = Severity .INFO ,
263+ line = last .line if last else 1 ,
264+ message = "Image declares EXPOSE/CMD/ENTRYPOINT but no HEALTHCHECK" ,
265+ hint = "Add `HEALTHCHECK CMD curl -fsS http://localhost:PORT/health || exit 1` so orchestrators can detect zombies." ,
266+ )
267+
268+
269+ def _rule_apt_get_install_no_y (instrs : List [Instruction ]) -> Iterable [Finding ]:
270+ for ins in instrs :
271+ if ins .cmd != "RUN" :
272+ continue
273+ low = ins .args .lower ()
274+ if "apt-get install" in low and not re .search (r"-y\b|--yes\b|--assume-yes\b" , low ):
275+ yield Finding (
276+ rule_id = "PR011" ,
277+ severity = Severity .MEDIUM ,
278+ line = ins .line ,
279+ message = "apt-get install without -y will block the build on the prompt" ,
280+ hint = "Add `-y` (or `--yes`) so non-interactive builds don't hang." ,
281+ )
282+
283+
284+ def _rule_pip_no_cache (instrs : List [Instruction ]) -> Iterable [Finding ]:
285+ for ins in instrs :
286+ if ins .cmd != "RUN" :
287+ continue
288+ low = ins .args .lower ()
289+ if re .search (r"\bpip(?:3)?\s+install\b" , low ) and "--no-cache-dir" not in low :
290+ yield Finding (
291+ rule_id = "PR012" ,
292+ severity = Severity .LOW ,
293+ line = ins .line ,
294+ message = "pip install without --no-cache-dir bloats the layer" ,
295+ hint = "Add `--no-cache-dir` so the wheel cache doesn't ship inside the image." ,
296+ )
297+
298+
224299RULES : Tuple [RuleFn , ...] = (
225300 _rule_no_latest_tag ,
226301 _rule_runs_as_root ,
@@ -229,6 +304,11 @@ def _rule_privileged_sudo(instrs: List[Instruction]) -> Iterable[Finding]:
229304 _rule_add_instead_of_copy ,
230305 _rule_hardcoded_secret ,
231306 _rule_privileged_sudo ,
307+ _rule_relative_workdir ,
308+ _rule_chmod_777 ,
309+ _rule_missing_healthcheck ,
310+ _rule_apt_get_install_no_y ,
311+ _rule_pip_no_cache ,
232312)
233313
234314
0 commit comments