Skip to content

Commit 0264477

Browse files
committed
feat: baseline test
1 parent 6e98fcc commit 0264477

File tree

4 files changed

+232
-1
lines changed

4 files changed

+232
-1
lines changed

nix/packages/cis-audit/scanner/internal/config/defaults.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,29 @@ var DefaultExclusions = Config{
3131
"/boot/config-*",
3232
"/boot/initrd.img-*",
3333
"/boot/vmlinuz-*",
34+
35+
// Development headers (not security-relevant)
36+
"/usr/include/*",
37+
38+
// Python cache files (regenerated, not security-relevant)
39+
"*/__pycache__/*",
40+
"*.pyc",
41+
42+
// User cache directories (dynamic, user-specific)
43+
"*/.cache/*",
44+
45+
// Nix build logs and var (deployment artifacts)
46+
"/nix/var/*",
47+
48+
// Dynamic linker cache (regenerated)
49+
"/etc/ld.so.cache",
50+
51+
// Shell history (dynamic, user-specific)
52+
"*/.bash_history",
53+
"*/.zsh_history",
54+
55+
// Ansible cache (deployment artifacts)
56+
"*/.ansible/*",
3457
},
3558

3659
ShallowDirs: []string{
@@ -39,6 +62,13 @@ var DefaultExclusions = Config{
3962

4063
// PostgreSQL data directory - contents are dynamic database state
4164
"/data/pgdata",
65+
66+
// Deployment/provisioning tools - internal implementation details
67+
"/opt/saltstack",
68+
69+
// Locally installed software - deep internals not security-relevant
70+
"/usr/local/share",
71+
"/usr/local/lib",
4272
},
4373

4474
KernelParams: []string{

nix/packages/cis-audit/scanner/internal/config/loader.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,45 @@ func removeItems(slice []string, itemsToRemove []string) []string {
181181
}
182182

183183
// IsPathExcluded checks if a given path matches any exclusion pattern.
184-
// Supports glob patterns (*, ?, []).
184+
// Supports glob patterns (*, ?, []) and special patterns:
185+
// - /dir/* matches anything under /dir/
186+
// - */__pycache__/* matches __pycache__ directories anywhere
187+
// - *.pyc matches any .pyc file
188+
// - */.bash_history matches .bash_history file anywhere
185189
func (c *Config) IsPathExcluded(path string) bool {
186190
for _, pattern := range c.Paths {
191+
// Handle patterns that match anywhere in the path (starting with *)
192+
if strings.HasPrefix(pattern, "*") {
193+
// Pattern like *.pyc - match file extension
194+
if strings.HasPrefix(pattern, "*.") {
195+
suffix := strings.TrimPrefix(pattern, "*")
196+
if strings.HasSuffix(path, suffix) {
197+
return true
198+
}
199+
continue
200+
}
201+
// Pattern like */__pycache__/* or */.cache/* - match directory component anywhere
202+
// Pattern like */.bash_history - match file anywhere
203+
if strings.HasPrefix(pattern, "*/") {
204+
// Get the part after */
205+
rest := strings.TrimPrefix(pattern, "*/")
206+
207+
if strings.HasSuffix(pattern, "/*") {
208+
// Directory pattern: */.cache/* should match /.cache/ anywhere
209+
dirName := strings.TrimSuffix(rest, "/*")
210+
if strings.Contains(path, "/"+dirName+"/") {
211+
return true
212+
}
213+
} else {
214+
// File pattern: */.bash_history should match /.bash_history at end
215+
if strings.HasSuffix(path, "/"+rest) {
216+
return true
217+
}
218+
}
219+
continue
220+
}
221+
}
222+
187223
matched, err := filepath.Match(pattern, path)
188224
if err != nil {
189225
// Invalid pattern, skip

nix/packages/cis-audit/scanner/internal/config/loader_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,84 @@ func TestLoad_ShallowDirs(t *testing.T) {
217217
}
218218
}
219219

220+
func TestIsPathExcluded_PycacheAndPyc(t *testing.T) {
221+
cfg := &Config{
222+
Paths: []string{
223+
"*/__pycache__/*",
224+
"*.pyc",
225+
},
226+
}
227+
228+
// Should be excluded
229+
excluded := []string{
230+
"/opt/saltstack/salt/lib/python3.10/__pycache__/locks.cpython-310.pyc",
231+
"/usr/lib/python3/__pycache__/abc.cpython-310.pyc",
232+
"/home/user/project/__pycache__/module.pyc",
233+
"/some/path/file.pyc",
234+
"/another/deeply/nested/file.pyc",
235+
}
236+
237+
for _, path := range excluded {
238+
if !cfg.IsPathExcluded(path) {
239+
t.Errorf("Expected %s to be excluded", path)
240+
}
241+
}
242+
243+
// Should NOT be excluded
244+
notExcluded := []string{
245+
"/opt/saltstack/salt/lib/python3.10/locks.py",
246+
"/usr/lib/python3/abc.py",
247+
"/etc/passwd",
248+
"/home/user/project/module.py",
249+
}
250+
251+
for _, path := range notExcluded {
252+
if cfg.IsPathExcluded(path) {
253+
t.Errorf("Expected %s to NOT be excluded", path)
254+
}
255+
}
256+
}
257+
258+
func TestIsPathExcluded_CacheAndHistory(t *testing.T) {
259+
cfg := &Config{
260+
Paths: []string{
261+
"*/.cache/*",
262+
"*/.bash_history",
263+
"*/.ansible/*",
264+
},
265+
}
266+
267+
// Should be excluded
268+
excluded := []string{
269+
"/home/ubuntu/.cache/nix/eval-cache-v6/something.sqlite",
270+
"/home/wal-g/.cache/nix/fetcher-cache-v4.sqlite",
271+
"/root/.cache/pip/something",
272+
"/home/ubuntu/.bash_history",
273+
"/root/.bash_history",
274+
"/home/ubuntu/.ansible/galaxy_cache/api.json",
275+
}
276+
277+
for _, path := range excluded {
278+
if !cfg.IsPathExcluded(path) {
279+
t.Errorf("Expected %s to be excluded", path)
280+
}
281+
}
282+
283+
// Should NOT be excluded
284+
notExcluded := []string{
285+
"/home/ubuntu/.bashrc",
286+
"/home/ubuntu/.profile",
287+
"/etc/passwd",
288+
"/home/ubuntu/cache/not-dotcache",
289+
}
290+
291+
for _, path := range notExcluded {
292+
if cfg.IsPathExcluded(path) {
293+
t.Errorf("Expected %s to NOT be excluded", path)
294+
}
295+
}
296+
}
297+
220298
// Helper function to check if a slice contains a string
221299
func contains(slice []string, item string) bool {
222300
for _, s := range slice {

testinfra/test_ami_nix.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from ec2instanceconnectcli.EC2InstanceConnectKey import EC2InstanceConnectKey
1111
from time import sleep
1212
import paramiko
13+
from pathlib import Path
1314

1415
# if EXECUTION_ID is not set, use a default value that includes the user and hostname
1516
RUN_ID = os.environ.get(
@@ -219,6 +220,16 @@ def run_ssh_command(ssh, command, timeout=None):
219220
}
220221

221222

223+
def upload_file_via_sftp(ssh, local_path, remote_path):
224+
"""Upload a file to the remote host via SFTP."""
225+
sftp = ssh.open_sftp()
226+
try:
227+
sftp.put(local_path, remote_path)
228+
logger.info(f"Uploaded {local_path} to {remote_path}")
229+
finally:
230+
sftp.close()
231+
232+
222233
# scope='session' uses the same container for all the tests;
223234
# scope='function' uses a new container per test function.
224235
@pytest.fixture(scope="session")
@@ -813,3 +824,79 @@ def test_postgrest_read_only_session_attrs(host):
813824
print("Warning: Failed to restart PostgreSQL after restoring config")
814825
else:
815826
print("Warning: Failed to restore PostgreSQL configuration")
827+
828+
829+
def test_cis_baseline_audit(host):
830+
"""Run CIS baseline audit against the machine and report results.
831+
832+
This test uploads the current baseline.yml from the repo and uses
833+
cis-audit to validate the machine against it. The test reports findings
834+
but does not fail the build - it's for visibility into configuration drift.
835+
"""
836+
git_sha = os.environ.get("GITHUB_SHA", "HEAD")
837+
838+
# Find the baseline file relative to the test file location
839+
test_dir = Path(__file__).parent.parent
840+
baseline_path = test_dir / "audit-specs" / "baselines" / "baseline.yml"
841+
842+
if not baseline_path.exists():
843+
print(f"\n⚠️ Baseline file not found at {baseline_path}")
844+
print("Skipping CIS baseline audit - no baseline file available")
845+
pytest.skip("Baseline file not found")
846+
return
847+
848+
print(f"\n{'='*60}")
849+
print("CIS BASELINE AUDIT")
850+
print(f"{'='*60}")
851+
print(f"Baseline file: {baseline_path}")
852+
853+
# Upload baseline file to the instance
854+
remote_baseline_path = "/tmp/baseline.yml"
855+
try:
856+
upload_file_via_sftp(host["ssh"], str(baseline_path), remote_baseline_path)
857+
print(f"✓ Uploaded baseline to {remote_baseline_path}")
858+
except Exception as e:
859+
print(f"✗ Failed to upload baseline file: {e}")
860+
pytest.skip(f"Failed to upload baseline: {e}")
861+
return
862+
863+
# Install cis-audit via nix
864+
print("\nInstalling cis-audit tool...")
865+
install_cmd = f"nix profile install github:supabase/postgres/{git_sha}#cis-audit --refresh 2>&1"
866+
result = run_ssh_command(host["ssh"], install_cmd, timeout=300)
867+
if not result["succeeded"]:
868+
print(f"Warning: {result['stderr'][:500]}")
869+
870+
# Run cis-audit with documentation format for readable output
871+
print("\nRunning CIS baseline validation...")
872+
print(f"{'-'*60}")
873+
874+
# Use the uploaded baseline file (local path, not bundled)
875+
validate_cmd = f"~/.nix-profile/bin/cis-audit --spec {remote_baseline_path} --format documentation 2>&1"
876+
result = run_ssh_command(host["ssh"], validate_cmd, timeout=600)
877+
878+
# Print full output for visibility in GitHub Actions logs
879+
print(result["stdout"])
880+
if result["stderr"]:
881+
print(f"\nStderr:\n{result['stderr']}")
882+
883+
print(f"{'-'*60}")
884+
885+
# Also run with tap format to get summary counts
886+
validate_tap_cmd = f"~/.nix-profile/bin/cis-audit --spec {remote_baseline_path} --format tap 2>&1 | tail -10"
887+
result_tap = run_ssh_command(host["ssh"], validate_tap_cmd, timeout=600)
888+
889+
print(f"\nSummary:")
890+
print(result_tap["stdout"])
891+
892+
# Clean up
893+
run_ssh_command(host["ssh"], f"rm -f {remote_baseline_path}")
894+
895+
print(f"{'='*60}")
896+
print("CIS BASELINE AUDIT COMPLETE")
897+
print(f"{'='*60}\n")
898+
899+
# Note: This test intentionally does not assert/fail on validation results
900+
# It's meant to provide visibility into configuration state
901+
# To make this test fail on drift, uncomment the following:
902+
# assert result["succeeded"], "CIS baseline validation found differences"

0 commit comments

Comments
 (0)