Skip to content

Commit fab936d

Browse files
ci: add pre-commit check ensuring FIPS compliance
this commit adds a new script to scan for non-FIPS compliant function usage within llama-stack as well as a new pre-commit hook to run the script automatically Assisted-by: claude-4-sonnet Signed-off-by: Nathan Weinberg <[email protected]>
1 parent 471b1b2 commit fab936d

File tree

2 files changed

+202
-1
lines changed

2 files changed

+202
-1
lines changed

.pre-commit-config.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,6 @@ repos:
153153
files: ^src/llama_stack/ui/.*\.(ts|tsx)$
154154
pass_filenames: false
155155
require_serial: true
156-
157156
- id: check-log-usage
158157
name: Ensure 'llama_stack.log' usage for logging
159158
entry: bash
@@ -172,6 +171,13 @@ repos:
172171
exit 1
173172
fi
174173
exit 0
174+
- id: fips-compliance
175+
name: Ensure llama-stack remains FIPS compliant
176+
entry: ./scripts/fips-check.py
177+
language: python
178+
pass_filenames: false
179+
require_serial: true
180+
files: ^src/llama_stack/.*$
175181

176182
ci:
177183
autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks

scripts/fips-check.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#!/usr/bin/env python
2+
# Copyright (c) Meta Platforms, Inc. and affiliates.
3+
# All rights reserved.
4+
#
5+
# This source code is licensed under the terms described in the LICENSE file in
6+
# the root directory of this source tree.
7+
"""
8+
FIPS Compliance Checker
9+
10+
This script scans all Python code within the llama_stack directory to check
11+
for usage of the following FIPS-non-compliant cryptographic functions:
12+
- hashlib.md5
13+
- hashlib.sha1
14+
- uuid.uuid3
15+
- uuid.uuid5
16+
17+
Exit codes:
18+
- 0: No prohibited functions found
19+
- 1: One or more prohibited functions found
20+
"""
21+
22+
import ast
23+
import os
24+
import sys
25+
from pathlib import Path
26+
27+
REPO_ROOT = Path(__file__).parent.parent
28+
29+
30+
class FIPSViolationFinder(ast.NodeVisitor):
31+
"""AST visitor to find FIPS-non-compliant function calls."""
32+
33+
def __init__(self):
34+
self.violations: list[tuple[str, int, str]] = []
35+
self.current_file = ""
36+
37+
# Prohibited functions we're looking for
38+
self.prohibited_functions = {"hashlib.md5", "hashlib.sha1", "uuid.uuid3", "uuid.uuid5"}
39+
40+
# Track imports to handle different import styles
41+
self.hashlib_imported = False
42+
self.uuid_imported = False
43+
self.hashlib_alias = None
44+
self.uuid_alias = None
45+
46+
def visit_import(self, node):
47+
"""Handle access case
48+
'import <module>'
49+
"""
50+
for alias in node.names:
51+
if alias.name == "hashlib":
52+
self.hashlib_imported = True
53+
self.hashlib_alias = alias.asname or "hashlib"
54+
elif alias.name == "uuid":
55+
self.uuid_imported = True
56+
self.uuid_alias = alias.asname or "uuid"
57+
self.generic_visit(node)
58+
59+
def visit_import_from(self, node):
60+
"""Handle access case
61+
'from <module> import <function>'
62+
"""
63+
if node.module == "hashlib":
64+
for alias in node.names:
65+
if alias.name in ["md5", "sha1"]:
66+
violation = f"from hashlib import {alias.name}"
67+
if alias.asname:
68+
violation += f" as {alias.asname}"
69+
self.violations.append((self.current_file, node.lineno, violation))
70+
elif node.module == "uuid":
71+
for alias in node.names:
72+
if alias.name in ["uuid3", "uuid5"]:
73+
violation = f"from uuid import {alias.name}"
74+
if alias.asname:
75+
violation += f" as {alias.asname}"
76+
self.violations.append((self.current_file, node.lineno, violation))
77+
self.generic_visit(node)
78+
79+
def visit_attribute(self, node):
80+
"""Handle access case
81+
'<module>.<function>'
82+
"""
83+
if isinstance(node.value, ast.Name):
84+
# Direct module.function access
85+
module_name = node.value.id
86+
attr_name = node.attr
87+
# Check for hashlib violations
88+
if (module_name == "hashlib" or module_name == self.hashlib_alias) and attr_name in ["md5", "sha1"]:
89+
violation = f"{module_name}.{attr_name}"
90+
self.violations.append((self.current_file, node.lineno, violation))
91+
# Check for uuid violations
92+
elif (module_name == "uuid" or module_name == self.uuid_alias) and attr_name in ["uuid3", "uuid5"]:
93+
violation = f"{module_name}.{attr_name}"
94+
self.violations.append((self.current_file, node.lineno, violation))
95+
self.generic_visit(node)
96+
97+
def visit_call(self, node):
98+
"""Handle function calls to catch direct calls to prohibited functions."""
99+
if isinstance(node.func, ast.Attribute):
100+
self.visit_attribute(node.func)
101+
elif isinstance(node.func, ast.Name):
102+
# Handle cases where functions were imported directly
103+
func_name = node.func.id
104+
if func_name in ["md5", "sha1", "uuid3", "uuid5"]:
105+
# This could be a prohibited function if imported directly
106+
violation = f"{func_name}()"
107+
self.violations.append((self.current_file, node.lineno, violation))
108+
109+
self.generic_visit(node)
110+
111+
112+
def scan_python_file(file_path: Path) -> list[tuple[str, int, str]]:
113+
"""Scan a single Python file for FIPS violations."""
114+
try:
115+
with open(file_path, encoding="utf-8") as f:
116+
content = f.read()
117+
118+
# Parse the AST
119+
tree = ast.parse(content, filename=str(file_path))
120+
121+
# Find violations
122+
finder = FIPSViolationFinder()
123+
finder.current_file = str(file_path)
124+
finder.visit(tree)
125+
126+
return finder.violations
127+
128+
except (SyntaxError, UnicodeDecodeError) as e:
129+
print(f"Error: Could not parse {file_path}: {e}", file=sys.stderr)
130+
sys.exit(1)
131+
132+
133+
def find_python_files(directory: Path) -> list[Path]:
134+
"""Find all Python files in the given directory."""
135+
python_files = []
136+
137+
for root, dirs, files in os.walk(directory):
138+
# Skip common non-source directories
139+
dirs[:] = [
140+
d
141+
for d in dirs
142+
if d not in {".git", "__pycache__", ".pytest_cache", "node_modules", ".venv", "venv", ".tox"}
143+
]
144+
145+
for file in files:
146+
if file.endswith(".py"):
147+
python_files.append(Path(root) / file)
148+
149+
return python_files
150+
151+
152+
def main():
153+
"""Main function to scan for FIPS violations."""
154+
155+
llama_stack_dir = REPO_ROOT / "src" / "llama_stack"
156+
if not llama_stack_dir.exists():
157+
print(f"Error: llama_stack directory not found at {llama_stack_dir}", file=sys.stderr)
158+
sys.exit(1)
159+
160+
print(f"Scanning Python files in {llama_stack_dir} for FIPS violations...")
161+
162+
# Find all Python files
163+
python_files = find_python_files(llama_stack_dir)
164+
print(f"Found {len(python_files)} Python files to scan")
165+
166+
# Scan each file
167+
all_violations = []
168+
for file_path in python_files:
169+
violations = scan_python_file(file_path)
170+
all_violations.extend(violations)
171+
172+
# Report results
173+
if all_violations:
174+
print("\n❌ FIPS COMPLIANCE CHECK FAILED")
175+
print(f"Found {len(all_violations)} violation(s):\n")
176+
177+
for file_path, line_num, violation in all_violations:
178+
# Make path relative to project root for cleaner output
179+
rel_path = Path(file_path).relative_to(REPO_ROOT)
180+
print(f" {rel_path}:{line_num} - {violation}")
181+
182+
print("\nProhibited functions found:")
183+
print(" - hashlib.md5 and hashlib.sha1 are not FIPS-compliant")
184+
print(" - uuid.uuid3 and uuid.uuid5 use MD5 and SHA-1 respectively")
185+
print(" - Consider using hashlib.sha256, hashlib.sha384, or hashlib.sha512")
186+
print(" - Consider using uuid.uuid4 (random) or uuid.uuid1 (MAC-based)")
187+
sys.exit(1)
188+
else:
189+
print("\n✅ FIPS COMPLIANCE CHECK PASSED")
190+
print(f"No prohibited cryptographic functions found in {len(python_files)} files")
191+
sys.exit(0)
192+
193+
194+
if __name__ == "__main__":
195+
main()

0 commit comments

Comments
 (0)