audit-labs/audit-tools

A collection of scripts, queries, and other goodies you can use in an audit.

clone: git clone https://gitbay.org/audit-labs/audit-tools.git

fbf7718daa9030664bc62118541eb47000973d04

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-07-29T04:21:42Z

style: make repo ruff-clean

Resolve ruff check errors across the repo so the lint CI passes:

- Apply ruff autofixes: sort imports (I001), modernize type hints (UP006/
  UP035/UP045), drop a redundant int() cast (RUF046).
- Mark the two shebang scripts executable (EXE001).
- Add ruff.toml ignoring three rules that flag intentional patterns: BLE001
  (collectors deliberately catch broadly so one failing check never aborts a
  run), DTZ011 (local date used for date-stamped output folders), and S112
  (skip unavailable resources during collection). This also lets the inline
  BLE001 noqa comments be removed.

No behavior changes. ruff check and ruff format --check both pass; all tests pass.
 applications/aws/aws_password_policy/evaluate_policy.py | 16 ++++++++--------
 applications/github/audit.py                            |  2 +-
 applications/github/collectors/audit_log.py             |  2 +-
 project_management/dash/app.py                          |  2 +-
 ruff.toml                                               | 14 ++++++++++++++
 sampling/audit_sample.py                                |  4 +---
 sampling/sampling_tool/cli.py                           |  2 +-
 sampling/sampling_tool/io.py                            |  3 +--
 sampling/sampling_tool/manifest.py                      |  2 +-
 sampling/sampling_tool/validation.py                    |  2 +-
 sampling/stratified_sample.py                           |  3 ++-
 tui/app.py                                              |  2 +-
 tui/github_runner.py                                    |  4 ++--
 tui/gitlab_runner.py                                    |  4 ++--
 14 files changed, 37 insertions(+), 25 deletions(-)

diff --git a/applications/aws/aws_password_policy/evaluate_policy.py b/applications/aws/aws_password_policy/evaluate_policy.py
old mode 100644
new mode 100755
index d49dffd..6169a49
--- a/applications/aws/aws_password_policy/evaluate_policy.py
+++ b/applications/aws/aws_password_policy/evaluate_policy.py
@@ -21,7 +21,7 @@ import json
 import sys
 from datetime import datetime, timezone
 from pathlib import Path
-from typing import Any, Dict, List, Optional
+from typing import Any
 
 # ----------------------------------------------------------------------
 # Mapping of the 10 password‑policy fields we care about
@@ -49,7 +49,7 @@ def utc_now() -> datetime:
     return datetime.datetime.now(timezone.utc)
 
 
-def prompt_expected(field_type: str, description: str) -> Optional[Any]:
+def prompt_expected(field_type: str, description: str) -> Any | None:
     """
     Ask the auditor for the expected value.
     Returns:
@@ -78,7 +78,7 @@ def prompt_expected(field_type: str, description: str) -> Optional[Any]:
             return raw
 
 
-def evaluate(expect: Optional[Any], actual: Any, field_type: str) -> str:
+def evaluate(expect: Any | None, actual: Any, field_type: str) -> str:
     """Return PASS / FAIL / N/A."""
     if expect is None:
         return "N/A"
@@ -89,7 +89,7 @@ def evaluate(expect: Optional[Any], actual: Any, field_type: str) -> str:
     return "FAIL"
 
 
-def load_json(path: Path) -> Dict[str, Any]:
+def load_json(path: Path) -> dict[str, Any]:
     """Read the JSON file generated by the Bash script."""
     try:
         with path.open("r", encoding="utf-8") as fh:
@@ -100,8 +100,8 @@ def load_json(path: Path) -> Dict[str, Any]:
 
 def write_csv(
     out_path: Path,
-    metadata: Dict[str, Any],
-    rows: List[List[Any]],
+    metadata: dict[str, Any],
+    rows: list[list[Any]],
 ) -> None:
     """Write the CSV report, including a metadata header block."""
     with out_path.open("w", newline="", encoding="utf-8") as csvfile:
@@ -136,7 +136,7 @@ def main() -> None:
     # --------------------------------------------------------------
     # 1. Prompt the auditor for expectations
     # --------------------------------------------------------------
-    expectations: Dict[str, Optional[Any]] = {}
+    expectations: dict[str, Any | None] = {}
     print("\n=== Expected / Minimum Values (press <Enter> for N/A) ===\n")
     for _, key, friendly, typ in POLICY_FIELDS:
         expectations[key] = prompt_expected(typ, friendly)
@@ -144,7 +144,7 @@ def main() -> None:
     # --------------------------------------------------------------
     # 2. Build the CSV rows (including PASS/FAIL)
     # --------------------------------------------------------------
-    csv_rows: List[List[Any]] = []
+    csv_rows: list[list[Any]] = []
     for rule_no, key, friendly, typ in POLICY_FIELDS:
         expected = expectations[key]
         actual = policy.get(key, "(missing)")
diff --git a/applications/github/audit.py b/applications/github/audit.py
index fe13d9b..7870637 100644
--- a/applications/github/audit.py
+++ b/applications/github/audit.py
@@ -34,7 +34,7 @@ import sys
 from datetime import date
 
 import config
-from collectors import members, branch_protections, commits, audit_log
+from collectors import audit_log, branch_protections, commits, members
 from reporters import csv_reporter
 
 
diff --git a/applications/github/collectors/audit_log.py b/applications/github/collectors/audit_log.py
index f992513..f15fcf8 100644
--- a/applications/github/collectors/audit_log.py
+++ b/applications/github/collectors/audit_log.py
@@ -5,9 +5,9 @@ Requires GitHub Enterprise Cloud. Skips gracefully with a warning if not
 available.
 """
 
-from datetime import date, datetime, timezone, timedelta
 import json
 import sys
+from datetime import date, datetime, timedelta, timezone
 
 import requests
 
diff --git a/project_management/dash/app.py b/project_management/dash/app.py
index ea530a6..79b075b 100644
--- a/project_management/dash/app.py
+++ b/project_management/dash/app.py
@@ -3,9 +3,9 @@ Extensible dashboard for project status.
 """
 
 # Import packages
-from dash import Dash, html, dcc
 import pandas as pd
 import plotly.express as px
+from dash import Dash, dcc, html
 
 # Incorporate data
 df = pd.read_excel("project_data.xlsx")
diff --git a/ruff.toml b/ruff.toml
new file mode 100644
index 0000000..21b1a5a
--- /dev/null
+++ b/ruff.toml
@@ -0,0 +1,14 @@
+# Ruff configuration for audit-tools.
+#
+# A few lint rules are disabled because they flag patterns this project uses
+# deliberately:
+#
+#   BLE001 - The audit collectors and their CLI wrappers intentionally catch
+#            broad exceptions so that one failing check never aborts a whole
+#            audit run. The error is reported and collection continues.
+#   DTZ011 - date.today() is used to build human-facing, date-stamped output
+#            folder names, where the local date is the intended value.
+#   S112   - try/except/continue is used to skip resources that are unavailable
+#            during collection (e.g. a repo without the requested branch).
+[lint]
+ignore = ["BLE001", "DTZ011", "S112"]
diff --git a/sampling/audit_sample.py b/sampling/audit_sample.py
old mode 100644
new mode 100755
index d7882a6..d5022c4
--- a/sampling/audit_sample.py
+++ b/sampling/audit_sample.py
@@ -1,15 +1,13 @@
 #!/usr/bin/env python3
 """Command-line entrypoint for the audit sampling tool."""
 
-from pathlib import Path
 import sys
-
+from pathlib import Path
 
 if __package__ is None or __package__ == "":
     sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
 
 from sampling.sampling_tool.cli import main
 
-
 if __name__ == "__main__":
     raise SystemExit(main())
diff --git a/sampling/sampling_tool/cli.py b/sampling/sampling_tool/cli.py
index 591c5b5..8ab57ed 100644
--- a/sampling/sampling_tool/cli.py
+++ b/sampling/sampling_tool/cli.py
@@ -2,11 +2,11 @@
 
 from __future__ import annotations
 
+import sys
 from argparse import ArgumentParser, Namespace
 from datetime import datetime, timezone
 from pathlib import Path
 from types import SimpleNamespace
-import sys
 
 import pandas as pd
 
diff --git a/sampling/sampling_tool/io.py b/sampling/sampling_tool/io.py
index 038492f..c23c379 100644
--- a/sampling/sampling_tool/io.py
+++ b/sampling/sampling_tool/io.py
@@ -2,12 +2,11 @@
 
 from __future__ import annotations
 
-from pathlib import Path
 import hashlib
+from pathlib import Path
 
 import pandas as pd
 
-
 SUPPORTED_EXCEL_SUFFIXES = {".xlsx", ".xls", ".xlsm"}
 
 
diff --git a/sampling/sampling_tool/manifest.py b/sampling/sampling_tool/manifest.py
index 15cb9df..c830322 100644
--- a/sampling/sampling_tool/manifest.py
+++ b/sampling/sampling_tool/manifest.py
@@ -2,8 +2,8 @@
 
 from __future__ import annotations
 
-from pathlib import Path
 import json
+from pathlib import Path
 
 from . import __version__
 
diff --git a/sampling/sampling_tool/validation.py b/sampling/sampling_tool/validation.py
index 1ed790c..d63dd31 100644
--- a/sampling/sampling_tool/validation.py
+++ b/sampling/sampling_tool/validation.py
@@ -69,7 +69,7 @@ def validate_and_prepare(population: pd.DataFrame, options) -> ValidationResult:
         keep=False
     )
     duplicate_rows = working.loc[nonblank_ids].loc[duplicate_mask].copy()
-    duplicate_id_count = int(len(duplicate_rows))
+    duplicate_id_count = len(duplicate_rows)
     if duplicate_id_count:
         if options.dedupe_id == "fail":
             raise AuditSamplingError(
diff --git a/sampling/stratified_sample.py b/sampling/stratified_sample.py
index 1ca94f9..66e34e6 100644
--- a/sampling/stratified_sample.py
+++ b/sampling/stratified_sample.py
@@ -1,7 +1,8 @@
 # Import packages
-import pandas as pd
 import math
 
+import pandas as pd
+
 # Load data
 df = pd.read_csv("FILENAME_GOES_HERE.csv")
 
diff --git a/tui/app.py b/tui/app.py
index b4f74c5..542e3b7 100644
--- a/tui/app.py
+++ b/tui/app.py
@@ -226,7 +226,7 @@ class RunScreen(Screen):
                 keys,
                 lambda ev: self.app.call_from_thread(self._handle_event, ev),
             )
-        except Exception as e:  # noqa: BLE001 - report unexpected failures in the UI
+        except Exception as e:
             self.app.call_from_thread(self._log, f"[red]Run failed:[/] {e}")
         finally:
             self.app.call_from_thread(self._finish)
diff --git a/tui/github_runner.py b/tui/github_runner.py
index 7e2c568..f107db9 100644
--- a/tui/github_runner.py
+++ b/tui/github_runner.py
@@ -147,7 +147,7 @@ def run_audit(
         on_event(ProgressEvent("fetch", "Repo collaborators (shared cache)"))
         try:
             repo_collabs = members.fetch_repo_collaborators(org, cfg)
-        except Exception as e:  # noqa: BLE001 - surface, keep going
+        except Exception as e:
             on_event(
                 ProgressEvent(
                     "error", "Repo collaborators (shared cache)", message=str(e)
@@ -165,7 +165,7 @@ def run_audit(
                 rows = c.fn(org, cfg, branch)
             else:
                 rows = c.fn(org, cfg)
-        except Exception as e:  # noqa: BLE001 - one bad check shouldn't kill the run
+        except Exception as e:
             on_event(ProgressEvent("error", c.label, message=str(e)))
             sections.append((c.label, 0))
             continue
diff --git a/tui/gitlab_runner.py b/tui/gitlab_runner.py
index dde9796..be4c9d4 100644
--- a/tui/gitlab_runner.py
+++ b/tui/gitlab_runner.py
@@ -136,7 +136,7 @@ def run_audit(
         on_event(ProgressEvent("fetch", "Projects (shared cache)"))
         try:
             project_cache = projects.fetch_projects(group, cfg)
-        except Exception as e:  # noqa: BLE001 - surface, keep going
+        except Exception as e:
             on_event(ProgressEvent("error", "Projects (shared cache)", message=str(e)))
             project_cache = []
 
@@ -148,7 +148,7 @@ def run_audit(
                 rows = c.fn(group, cfg, project_cache or [])
             else:
                 rows = c.fn(group, cfg)
-        except Exception as e:  # noqa: BLE001 - one bad check shouldn't kill the run
+        except Exception as e:
             on_event(ProgressEvent("error", c.label, message=str(e)))
             sections.append((c.label, 0))
             continue