audit-labs/audit-tools

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

Commit cb8640beec

cb8640beeced7c564c367d6694c578b6acdeafc7

parent: 4867af4f3d

Unsigned

cmc <hello@cleberg.net> · 2026-08-07 03:04 UTC

Resolve SonarCloud maintainability findings

Mechanical clean-ups across the shipped scripts and tools:
- Shell: use [[ ]] tests, redirect error messages to stderr, add explicit
  returns, and assign positional params to locals (shelldre S7688/S7677/S7682/S7679)
- Python: silence interface-mandated collector params with a _ prefix, drop
  genuinely unused params in the sampling writers, and extract duplicated
  string literals into constants (S1172, S1192)
- Tests: split a composite assertion and move a non-throwing call out of a
  pytest.raises block (S9073, S5778)
- sample.html: prefer Number.parseInt / Number.isNaN over the globals (S7773)

Layout: unified · split

applications/aws/aws_iam_users.sh +12 −12
@@ -9,7 +9,7 @@ ACCOUNT_NAME=""
9 9
10# --- Prerequisite check --- 10# --- Prerequisite check ---
11if ! command -v aws &> /dev/null || ! command -v jq &> /dev/null; then 11if ! command -v aws &> /dev/null || ! command -v jq &> /dev/null; then
12 echo "Error: Both AWS CLI and jq are required. Please install them and ensure they are in your PATH." 12 echo "Error: Both AWS CLI and jq are required. Please install them and ensure they are in your PATH." >&2
13 exit 1 13 exit 1
14fi 14fi
15 15
@@ -19,15 +19,15 @@ echo "Fetching IAM Identity Center and Account details..."
19INSTANCE_ARN=$(aws sso-admin list-instances --query "Instances[0].InstanceArn" --output text) 19INSTANCE_ARN=$(aws sso-admin list-instances --query "Instances[0].InstanceArn" --output text)
20IDENTITY_STORE_ID=$(aws sso-admin list-instances --query "Instances[0].IdentityStoreId" --output text) 20IDENTITY_STORE_ID=$(aws sso-admin list-instances --query "Instances[0].IdentityStoreId" --output text)
21 21
22if [ -z "$INSTANCE_ARN" ] || [ -z "$IDENTITY_STORE_ID" ]; then 22if [[ -z "$INSTANCE_ARN" ]] || [[ -z "$IDENTITY_STORE_ID" ]]; then
23 echo "Error: Could not find IAM Identity Center instance ARN or Identity Store ID." 23 echo "Error: Could not find IAM Identity Center instance ARN or Identity Store ID." >&2
24 exit 1 24 exit 1
25fi 25fi
26 26
27ACCOUNT_ID=$(aws organizations list-accounts --query "Accounts[?Name=='$ACCOUNT_NAME' && Status=='ACTIVE'].Id" --output text) 27ACCOUNT_ID=$(aws organizations list-accounts --query "Accounts[?Name=='$ACCOUNT_NAME' && Status=='ACTIVE'].Id" --output text)
28 28
29if [ -z "$ACCOUNT_ID" ]; then 29if [[ -z "$ACCOUNT_ID" ]]; then
30 echo "Error: Could not find an active AWS account with the name '$ACCOUNT_NAME'." 30 echo "Error: Could not find an active AWS account with the name '$ACCOUNT_NAME'." >&2
31 exit 1 31 exit 1
32fi 32fi
33 33
@@ -41,7 +41,7 @@ PROVISIONED_SETS_ARN=$(aws sso-admin list-permission-sets-provisioned-to-account
41 --account-id "$ACCOUNT_ID" \ 41 --account-id "$ACCOUNT_ID" \
42 --query "PermissionSets[]" --output text) 42 --query "PermissionSets[]" --output text)
43 43
44if [ -z "$PROVISIONED_SETS_ARN" ]; then 44if [[ -z "$PROVISIONED_SETS_ARN" ]]; then
45 echo "No permission sets are provisioned for account '$ACCOUNT_NAME'." 45 echo "No permission sets are provisioned for account '$ACCOUNT_NAME'."
46 exit 0 46 exit 0
47fi 47fi
@@ -64,14 +64,14 @@ for PS_ARN in $PROVISIONED_SETS_ARN; do
64 --permission-set-arn "$PS_ARN" \ 64 --permission-set-arn "$PS_ARN" \
65 --query "AccountAssignments[]" --output json) 65 --query "AccountAssignments[]" --output json)
66 66
67 if [ "$(echo "$ACCOUNT_ASSIGNMENTS" | jq 'length')" -eq 0 ]; then 67 if [[ "$(echo "$ACCOUNT_ASSIGNMENTS" | jq 'length')" -eq 0 ]]; then
68 echo " -> Permission Set ARN $PS_ARN is provisioned but has no active assignments." 68 echo " -> Permission Set ARN $PS_ARN is provisioned but has no active assignments."
69 continue 69 continue
70 fi 70 fi
71 71
72 # Since there are assignments, let's get the permission set's details (policies, name) 72 # Since there are assignments, let's get the permission set's details (policies, name)
73 # Using a cache to avoid redundant calls if a PS is somehow listed twice 73 # Using a cache to avoid redundant calls if a PS is somehow listed twice
74 if [ -z "${PERMISSION_SET_CACHE[$PS_ARN]}" ]; then 74 if [[ -z "${PERMISSION_SET_CACHE[$PS_ARN]}" ]]; then
75 echo " -> Fetching policies for Permission Set: $PS_ARN" 75 echo " -> Fetching policies for Permission Set: $PS_ARN"
76 PS_NAME=$(aws sso-admin describe-permission-set --instance-arn "$INSTANCE_ARN" --permission-set-arn "$PS_ARN" --query "PermissionSet.Name" --output text) 76 PS_NAME=$(aws sso-admin describe-permission-set --instance-arn "$INSTANCE_ARN" --permission-set-arn "$PS_ARN" --query "PermissionSet.Name" --output text)
77 MANAGED_POLICIES=$(aws sso-admin list-managed-policies-in-permission-set --instance-arn "$INSTANCE_ARN" --permission-set-arn "$PS_ARN" --query "AttachedManagedPolicies[].Arn" --output json) 77 MANAGED_POLICIES=$(aws sso-admin list-managed-policies-in-permission-set --instance-arn "$INSTANCE_ARN" --permission-set-arn "$PS_ARN" --query "AttachedManagedPolicies[].Arn" --output json)
@@ -87,16 +87,16 @@ for PS_ARN in $PROVISIONED_SETS_ARN; do
87 87
88 # Now process each assignment found for this permission set 88 # Now process each assignment found for this permission set
89 for row in $(echo "${ACCOUNT_ASSIGNMENTS}" | jq -r '.[] | @base64'); do 89 for row in $(echo "${ACCOUNT_ASSIGNMENTS}" | jq -r '.[] | @base64'); do
90 _jq() { echo ${row} | base64 --decode | jq -r ${1}; } 90 _jq() { echo ${row} | base64 --decode | jq -r ${1}; return 0; }
91 PRINCIPAL_TYPE=$(_jq '.PrincipalType') 91 PRINCIPAL_TYPE=$(_jq '.PrincipalType')
92 PRINCIPAL_ID=$(_jq '.PrincipalId') 92 PRINCIPAL_ID=$(_jq '.PrincipalId')
93 93
94 # Get Principal (User/Group) Name, using a cache 94 # Get Principal (User/Group) Name, using a cache
95 if [ -z "${PRINCIPAL_NAME_CACHE[$PRINCIPAL_ID]}" ]; then 95 if [[ -z "${PRINCIPAL_NAME_CACHE[$PRINCIPAL_ID]}" ]]; then
96 PRINCIPAL_NAME="" 96 PRINCIPAL_NAME=""
97 if [ "$PRINCIPAL_TYPE" == "USER" ]; then 97 if [[ "$PRINCIPAL_TYPE" == "USER" ]]; then
98 PRINCIPAL_NAME=$(aws identitystore describe-user --identity-store-id "$IDENTITY_STORE_ID" --user-id "$PRINCIPAL_ID" --query "UserName" --output text 2>/dev/null) 98 PRINCIPAL_NAME=$(aws identitystore describe-user --identity-store-id "$IDENTITY_STORE_ID" --user-id "$PRINCIPAL_ID" --query "UserName" --output text 2>/dev/null)
99 elif [ "$PRINCIPAL_TYPE" == "GROUP" ]; then 99 elif [[ "$PRINCIPAL_TYPE" == "GROUP" ]]; then
100 PRINCIPAL_NAME=$(aws identitystore describe-group --identity-store-id "$IDENTITY_STORE_ID" --group-id "$PRINCIPAL_ID" --query "DisplayName" --output text 2>/dev/null) 100 PRINCIPAL_NAME=$(aws identitystore describe-group --identity-store-id "$IDENTITY_STORE_ID" --group-id "$PRINCIPAL_ID" --query "DisplayName" --output text 2>/dev/null)
101 fi 101 fi
102 PRINCIPAL_NAME_CACHE[$PRINCIPAL_ID]=${PRINCIPAL_NAME:-"ID: $PRINCIPAL_ID"} 102 PRINCIPAL_NAME_CACHE[$PRINCIPAL_ID]=${PRINCIPAL_NAME:-"ID: $PRINCIPAL_ID"}
applications/aws/aws_s3_buckets.sh +12 −12
@@ -18,7 +18,7 @@ echo "---"
18echo "1. Retrieving all bucket names..." 18echo "1. Retrieving all bucket names..."
19BUCKET_LIST=$(aws s3api list-buckets --region "$MASTER_REGION" --query 'Buckets[].Name' --output text) 19BUCKET_LIST=$(aws s3api list-buckets --region "$MASTER_REGION" --query 'Buckets[].Name' --output text)
20 20
21if [ -z "$BUCKET_LIST" ]; then 21if [[ -z "$BUCKET_LIST" ]]; then
22 echo "✅ No S3 buckets found in this account." 22 echo "✅ No S3 buckets found in this account."
23 exit 0 23 exit 0
24fi 24fi
@@ -32,14 +32,14 @@ for BUCKET_NAME in $BUCKET_LIST; do
32 # 2. Find the bucket region 32 # 2. Find the bucket region
33 for REGION in $AWS_REGIONS; do 33 for REGION in $AWS_REGIONS; do
34 BUCKET_LOCATION_RESPONSE=$(aws s3api get-bucket-location --bucket "$BUCKET_NAME" --region "$REGION" 2>/dev/null) 34 BUCKET_LOCATION_RESPONSE=$(aws s3api get-bucket-location --bucket "$BUCKET_NAME" --region "$REGION" 2>/dev/null)
35 if [ $? -eq 0 ]; then 35 if [[ $? -eq 0 ]]; then
36 LOCATION_CONSTRAINT=$(echo "$BUCKET_LOCATION_RESPONSE" | jq -r '.LocationConstraint') 36 LOCATION_CONSTRAINT=$(echo "$BUCKET_LOCATION_RESPONSE" | jq -r '.LocationConstraint')
37 BUCKET_REGION=${LOCATION_CONSTRAINT:-"us-east-1"} 37 BUCKET_REGION=${LOCATION_CONSTRAINT:-"us-east-1"}
38 break 38 break
39 fi 39 fi
40 done 40 done
41 41
42 if [ -z "$BUCKET_REGION" ]; then 42 if [[ -z "$BUCKET_REGION" ]]; then
43 echo " ⚠️ WARNING: Could not determine region for $BUCKET_NAME. Skipping all checks." 43 echo " ⚠️ WARNING: Could not determine region for $BUCKET_NAME. Skipping all checks."
44 echo "$BUCKET_NAME,UNKNOWN,N/A,N/A,N/A,N/A,UNKNOWN" >> "$REPORT_FILE" 44 echo "$BUCKET_NAME,UNKNOWN,N/A,N/A,N/A,N/A,UNKNOWN" >> "$REPORT_FILE"
45 continue 45 continue
@@ -57,14 +57,14 @@ for BUCKET_NAME in $BUCKET_LIST; do
57 # --- CHECK A: Public Access Block (PAB) --- 57 # --- CHECK A: Public Access Block (PAB) ---
58 PAB_STATUS=$(aws s3api get-public-access-block --bucket "$BUCKET_NAME" --region "$BUCKET_REGION" 2>/dev/null) 58 PAB_STATUS=$(aws s3api get-public-access-block --bucket "$BUCKET_NAME" --region "$BUCKET_REGION" 2>/dev/null)
59 59
60 if [ $? -ne 0 ]; then 60 if [[ $? -ne 0 ]]; then
61 # PAB Missing is the highest risk state. 61 # PAB Missing is the highest risk state.
62 PAB_FULLY_RESTRICTED="CRITICAL-MISSING" 62 PAB_FULLY_RESTRICTED="CRITICAL-MISSING"
63 OVERALL_PUBLIC_STATUS="TRUE - PAB Missing" 63 OVERALL_PUBLIC_STATUS="TRUE - PAB Missing"
64 else 64 else
65 # Check if ALL four PAB flags are true 65 # Check if ALL four PAB flags are true
66 PAB_CONFIG=$(echo "$PAB_STATUS" | jq -r '.PublicAccessBlockConfiguration') 66 PAB_CONFIG=$(echo "$PAB_STATUS" | jq -r '.PublicAccessBlockConfiguration')
67 if [ "$(echo "$PAB_CONFIG" | jq -r '.BlockPublicAcls and .IgnorePublicAcls and .BlockPublicPolicy and .RestrictPublicBuckets')" = "true" ]; then 67 if [[ "$(echo "$PAB_CONFIG" | jq -r '.BlockPublicAcls and .IgnorePublicAcls and .BlockPublicPolicy and .RestrictPublicBuckets')" = "true" ]]; then
68 PAB_FULLY_RESTRICTED="TRUE" 68 PAB_FULLY_RESTRICTED="TRUE"
69 else 69 else
70 PAB_FULLY_RESTRICTED="FALSE-VULNERABLE" 70 PAB_FULLY_RESTRICTED="FALSE-VULNERABLE"
@@ -74,9 +74,9 @@ for BUCKET_NAME in $BUCKET_LIST; do
74 # --- CHECK B: Bucket Policy Status (If S3 service thinks it's public) --- 74 # --- CHECK B: Bucket Policy Status (If S3 service thinks it's public) ---
75 POLICY_STATUS=$(aws s3api get-bucket-policy-status --bucket "$BUCKET_NAME" --region "$BUCKET_REGION" 2>/dev/null) 75 POLICY_STATUS=$(aws s3api get-bucket-policy-status --bucket "$BUCKET_NAME" --region "$BUCKET_REGION" 2>/dev/null)
76 76
77 if [ $? -eq 0 ]; then 77 if [[ $? -eq 0 ]]; then
78 POLICY_IS_PUBLIC=$(echo "$POLICY_STATUS" | jq -r '.PolicyStatus.IsPublic') 78 POLICY_IS_PUBLIC=$(echo "$POLICY_STATUS" | jq -r '.PolicyStatus.IsPublic')
79 if [ "$POLICY_IS_PUBLIC" = "true" ]; then 79 if [[ "$POLICY_IS_PUBLIC" = "true" ]]; then
80 OVERALL_PUBLIC_STATUS="TRUE - Policy" 80 OVERALL_PUBLIC_STATUS="TRUE - Policy"
81 fi 81 fi
82 else 82 else
@@ -87,13 +87,13 @@ for BUCKET_NAME in $BUCKET_LIST; do
87 # --- CHECK C: Bucket ACLs (for AllUsers group) --- 87 # --- CHECK C: Bucket ACLs (for AllUsers group) ---
88 ACL_RESPONSE=$(aws s3api get-bucket-acl --bucket "$BUCKET_NAME" --region "$BUCKET_REGION" 2>/dev/null) 88 ACL_RESPONSE=$(aws s3api get-bucket-acl --bucket "$BUCKET_NAME" --region "$BUCKET_REGION" 2>/dev/null)
89 89
90 if [ $? -eq 0 ]; then 90 if [[ $? -eq 0 ]]; then
91 # Find if any grant to 'http://acs.amazonaws.com/groups/global/AllUsers' exists 91 # Find if any grant to 'http://acs.amazonaws.com/groups/global/AllUsers' exists
92 92
93 # Check for READ access 93 # Check for READ access
94 if echo "$ACL_RESPONSE" | jq -e '.Grants[] | select(.Grantee.URI=="http://acs.amazonaws.com/groups/global/AllUsers") | select(.Permission | test("READ|FULL_CONTROL"))' >/dev/null; then 94 if echo "$ACL_RESPONSE" | jq -e '.Grants[] | select(.Grantee.URI=="http://acs.amazonaws.com/groups/global/AllUsers") | select(.Permission | test("READ|FULL_CONTROL"))' >/dev/null; then
95 ACL_ALL_USERS_READ="TRUE" 95 ACL_ALL_USERS_READ="TRUE"
96 if [ "$OVERALL_PUBLIC_STATUS" = "FALSE" ]; then 96 if [[ "$OVERALL_PUBLIC_STATUS" = "FALSE" ]]; then
97 OVERALL_PUBLIC_STATUS="TRUE - ACL Read" 97 OVERALL_PUBLIC_STATUS="TRUE - ACL Read"
98 fi 98 fi
99 fi 99 fi
@@ -101,7 +101,7 @@ for BUCKET_NAME in $BUCKET_LIST; do
101 # Check for WRITE access (often less common for public, but still public exposure) 101 # Check for WRITE access (often less common for public, but still public exposure)
102 if echo "$ACL_RESPONSE" | jq -e '.Grants[] | select(.Grantee.URI=="http://acs.amazonaws.com/groups/global/AllUsers") | select(.Permission | test("WRITE|FULL_CONTROL"))' >/dev/null; then 102 if echo "$ACL_RESPONSE" | jq -e '.Grants[] | select(.Grantee.URI=="http://acs.amazonaws.com/groups/global/AllUsers") | select(.Permission | test("WRITE|FULL_CONTROL"))' >/dev/null; then
103 ACL_ALL_USERS_WRITE="TRUE" 103 ACL_ALL_USERS_WRITE="TRUE"
104 if [ "$OVERALL_PUBLIC_STATUS" = "FALSE" ]; then 104 if [[ "$OVERALL_PUBLIC_STATUS" = "FALSE" ]]; then
105 OVERALL_PUBLIC_STATUS="TRUE - ACL Write" 105 OVERALL_PUBLIC_STATUS="TRUE - ACL Write"
106 fi 106 fi
107 fi 107 fi
@@ -111,9 +111,9 @@ for BUCKET_NAME in $BUCKET_LIST; do
111 fi 111 fi
112 112
113 # Final check for PAB failure (PAB is the highest authority) 113 # Final check for PAB failure (PAB is the highest authority)
114 if [ "$PAB_FULLY_RESTRICTED" = "CRITICAL-MISSING" ]; then 114 if [[ "$PAB_FULLY_RESTRICTED" = "CRITICAL-MISSING" ]]; then
115 OVERALL_PUBLIC_STATUS="TRUE - PAB Missing (CRITICAL)" 115 OVERALL_PUBLIC_STATUS="TRUE - PAB Missing (CRITICAL)"
116 elif [ "$OVERALL_PUBLIC_STATUS" != "FALSE" ] && [ "$PAB_FULLY_RESTRICTED" != "TRUE" ]; then 116 elif [[ "$OVERALL_PUBLIC_STATUS" != "FALSE" ]] && [[ "$PAB_FULLY_RESTRICTED" != "TRUE" ]]; then
117 # If the bucket is found public by Policy or ACL AND PAB isn't fully set, confirm it's public 117 # If the bucket is found public by Policy or ACL AND PAB isn't fully set, confirm it's public
118 : # Status already set by Policy or ACL check above 118 : # Status already set by Policy or ACL check above
119 fi 119 fi
applications/github/collectors/members.py +2 −2
@@ -132,7 +132,7 @@ def outside_collaborators(org, cfg, repo_collabs):
132 return rows 132 return rows
133 133
134 134
135def privileged_access(org, cfg, repo_collabs): 135def privileged_access(_org, _cfg, repo_collabs):
136 """ 136 """
137 All users with admin permission on any repo. 137 All users with admin permission on any repo.
138 Accepts pre-fetched repo_collabs from fetch_repo_collaborators(). 138 Accepts pre-fetched repo_collabs from fetch_repo_collaborators().
@@ -196,7 +196,7 @@ def team_permissions(org, cfg):
196 return rows 196 return rows
197 197
198 198
199def permission_matrix(org, cfg, repo_collabs): 199def permission_matrix(_org, _cfg, repo_collabs):
200 """ 200 """
201 Full per-repo/per-user permission cross-reference. 201 Full per-repo/per-user permission cross-reference.
202 Accepts pre-fetched repo_collabs from fetch_repo_collaborators(). 202 Accepts pre-fetched repo_collabs from fetch_repo_collaborators().
applications/gitlab/collectors/approvals.py +1 −1
@@ -12,7 +12,7 @@ import requests
12from .api import paginate 12from .api import paginate
13 13
14 14
15def approval_rules(group, cfg, projects): 15def approval_rules(_group, cfg, projects):
16 rows = [] 16 rows = []
17 for p in projects: 17 for p in projects:
18 try: 18 try:
applications/gitlab/collectors/branch_protections.py +1 −1
@@ -12,7 +12,7 @@ def _levels(entries):
12 return ", ".join(e.get("access_level_description", "") for e in entries) or "(none)" 12 return ", ".join(e.get("access_level_description", "") for e in entries) or "(none)"
13 13
14 14
15def branch_protections(group, cfg, projects): 15def branch_protections(_group, cfg, projects):
16 rows = [] 16 rows = []
17 for p in projects: 17 for p in projects:
18 try: 18 try:
applications/gitlab/collectors/members.py +1 −1
@@ -44,7 +44,7 @@ def group_members(group, cfg):
44 ] 44 ]
45 45
46 46
47def project_members(group, cfg, projects): 47def project_members(_group, cfg, projects):
48 """Direct and inherited members of every project in the group.""" 48 """Direct and inherited members of every project in the group."""
49 rows = [] 49 rows = []
50 for p in projects: 50 for p in projects:
applications/gitlab/collectors/pipelines.py +1 −1
@@ -7,7 +7,7 @@ import requests
7from .api import paginate 7from .api import paginate
8 8
9 9
10def pipelines(group, cfg, projects): 10def pipelines(_group, cfg, projects):
11 rows = [] 11 rows = []
12 for p in projects: 12 for p in projects:
13 try: 13 try:
applications/gitlab/collectors/projects.py +1 −1
@@ -17,7 +17,7 @@ def fetch_projects(group, cfg):
17 ) 17 )
18 18
19 19
20def project_list(group, cfg, projects): 20def project_list(_group, _cfg, projects):
21 """Format the project cache into audit rows.""" 21 """Format the project cache into audit rows."""
22 return [ 22 return [
23 { 23 {
databases/sql/passwords/passwords.py +15 −10
@@ -5,6 +5,11 @@ Checks SQL Server user data for compliance with Windows policies.
5# Import packages 5# Import packages
6import pandas as pd 6import pandas as pd
7 7
8# Report column labels (defined once to avoid duplicated string literals).
9TYPE_CHECK = "Type Check"
10POLICY_CHECK = "Policy Check"
11EXPIRATION_CHECK = "Expiration Check"
12
8# Load the data into a pandas DataFrame 13# Load the data into a pandas DataFrame
9df_input = pd.read_csv("./data.csv") 14df_input = pd.read_csv("./data.csv")
10 15
@@ -24,38 +29,38 @@ def apply_rules_and_report(df):
24 for _, row in df.iterrows(): 29 for _, row in df.iterrows():
25 result = { 30 result = {
26 "Name": row["name"], 31 "Name": row["name"],
27 "Type Check": "", 32 TYPE_CHECK: "",
28 "Policy Check": "", 33 POLICY_CHECK: "",
29 "Expiration Check": "", 34 EXPIRATION_CHECK: "",
30 "Reason": "", 35 "Reason": "",
31 } 36 }
32 37
33 # Check the type_desc 38 # Check the type_desc
34 if row["type_desc"] == "SQL_LOGIN": 39 if row["type_desc"] == "SQL_LOGIN":
35 result["Type Check"] = "SQL_LOGIN" 40 result[TYPE_CHECK] = "SQL_LOGIN"
36 elif row["type_desc"] == "WINDOWS_LOGIN": 41 elif row["type_desc"] == "WINDOWS_LOGIN":
37 result["Type Check"] = "N/A" 42 result[TYPE_CHECK] = "N/A"
38 result["Reason"] = "Refer to Windows password policy." 43 result["Reason"] = "Refer to Windows password policy."
39 else: 44 else:
40 result["Type Check"] = "Manual Review" 45 result[TYPE_CHECK] = "Manual Review"
41 result["Reason"] = "Reviewer to manually review." 46 result["Reason"] = "Reviewer to manually review."
42 47
43 # Check if password policy is enforced 48 # Check if password policy is enforced
44 if row["is_policy_checked"] == 1: 49 if row["is_policy_checked"] == 1:
45 result["Policy Check"] = "PASS" 50 result[POLICY_CHECK] = "PASS"
46 result["Reason"] += """Password policy is enforced. Reviewer to 51 result["Reason"] += """Password policy is enforced. Reviewer to
47 check the assigned policy.""" 52 check the assigned policy."""
48 else: 53 else:
49 result["Policy Check"] = "FAIL" 54 result[POLICY_CHECK] = "FAIL"
50 result["Reason"] += "Password policy is not enforced." 55 result["Reason"] += "Password policy is not enforced."
51 56
52 # Check if password expiration is enforced 57 # Check if password expiration is enforced
53 if row["is_expiration_checked"] == 1: 58 if row["is_expiration_checked"] == 1:
54 result["Expiration Check"] = "PASS" 59 result[EXPIRATION_CHECK] = "PASS"
55 result["Reason"] += """Password expiration is enforced. Reviewer to 60 result["Reason"] += """Password expiration is enforced. Reviewer to
56 check the expiration policy.""" 61 check the expiration policy."""
57 else: 62 else:
58 result["Expiration Check"] = "FAIL" 63 result[EXPIRATION_CHECK] = "FAIL"
59 result["Reason"] += "Password expiration is not enforced." 64 result["Reason"] += "Password expiration is not enforced."
60 65
61 report.append(result) 66 report.append(result)
os/linux/passwords.sh +5 −3
@@ -4,11 +4,11 @@
4extract_password_params() { 4extract_password_params() {
5 echo "Checking /etc/pam.d/system-auth for password parameters..." 5 echo "Checking /etc/pam.d/system-auth for password parameters..."
6 6
7 if [ -f /etc/pam.d/system-auth ]; then 7 if [[ -f /etc/pam.d/system-auth ]]; then
8 # Extract the line containing the password complexity parameters 8 # Extract the line containing the password complexity parameters
9 param_line=$(grep -E 'difok=.* minlen=.* dcredit=.* ocredit=.* ucredit=.* lcredit=.* minclass=.* maxsequence=.*' /etc/pam.d/system-auth) 9 param_line=$(grep -E 'difok=.* minlen=.* dcredit=.* ocredit=.* ucredit=.* lcredit=.* minclass=.* maxsequence=.*' /etc/pam.d/system-auth)
10 10
11 if [ -n "$param_line" ]; then 11 if [[ -n "$param_line" ]]; then
12 echo "Password complexity parameters found:" 12 echo "Password complexity parameters found:"
13 echo "$param_line" 13 echo "$param_line"
14 echo "" 14 echo ""
@@ -44,12 +44,13 @@ extract_password_params() {
44 else 44 else
45 echo "/etc/pam.d/system-auth file not found." 45 echo "/etc/pam.d/system-auth file not found."
46 fi 46 fi
47 return 0
47} 48}
48 49
49# Function to analyze /etc/login.defs 50# Function to analyze /etc/login.defs
50analyze_login_defs() { 51analyze_login_defs() {
51 echo "Analyzing /etc/login.defs..." 52 echo "Analyzing /etc/login.defs..."
52 if [ -f /etc/login.defs ]; then 53 if [[ -f /etc/login.defs ]]; then
53 echo "Contents of /etc/login.defs:" 54 echo "Contents of /etc/login.defs:"
54 cat /etc/login.defs 55 cat /etc/login.defs
55 echo "" 56 echo ""
@@ -61,6 +62,7 @@ analyze_login_defs() {
61 else 62 else
62 echo "/etc/login.defs file not found." 63 echo "/etc/login.defs file not found."
63 fi 64 fi
65 return 0
64} 66}
65 67
66# Main script execution 68# Main script execution
os/linux/report/linux.sh +10 −3
@@ -6,10 +6,13 @@ TRIM_COMMENTS=false
6 6
7# Function to log section header 7# Function to log section header
8log_section() { 8log_section() {
9 local section_num="$1"
10 local section_title="$2"
9 echo -e "\n\n" >> "$REPORT_FILE" 11 echo -e "\n\n" >> "$REPORT_FILE"
10 echo "==========================================" >> "$REPORT_FILE" 12 echo "==========================================" >> "$REPORT_FILE"
11 echo "# SECTION $1: $2" >> "$REPORT_FILE" 13 echo "# SECTION $section_num: $section_title" >> "$REPORT_FILE"
12 echo "==========================================" >> "$REPORT_FILE" 14 echo "==========================================" >> "$REPORT_FILE"
15 return 0
13} 16}
14 17
15# Function to log file content 18# Function to log file content
@@ -27,12 +30,16 @@ log_file_content() {
27 else 30 else
28 echo "File $FILE_PATH not found!" >> "$REPORT_FILE" 31 echo "File $FILE_PATH not found!" >> "$REPORT_FILE"
29 fi 32 fi
33 return 0
30} 34}
31 35
32# Function to log command output 36# Function to log command output
33log_command_output() { 37log_command_output() {
34 echo "## $1" >> "$REPORT_FILE" 38 local label="$1"
35 $2 >> "$REPORT_FILE" 2>&1 39 local command="$2"
40 echo "## $label" >> "$REPORT_FILE"
41 $command >> "$REPORT_FILE" 2>&1
42 return 0
36} 43}
37 44
38# Check for sudo privileges 45# Check for sudo privileges
os/linux/ssh_root_login.sh +6 −6
@@ -1,14 +1,14 @@
1#!/bin/bash 1#!/bin/bash
2 2
3# Check if the script is being run as root 3# Check if the script is being run as root
4if [ "$EUID" -ne 0 ]; then 4if [[ "$EUID" -ne 0 ]]; then
5 echo "Error: This script must be run as root or with sudo." 5 echo "Error: This script must be run as root or with sudo." >&2
6 exit 1 6 exit 1
7fi 7fi
8 8
9# Check if the sshd_config file exists 9# Check if the sshd_config file exists
10if [ ! -f /etc/ssh/sshd_config ]; then 10if [[ ! -f /etc/ssh/sshd_config ]]; then
11 echo "Error: /etc/ssh/sshd_config not found." 11 echo "Error: /etc/ssh/sshd_config not found." >&2
12 exit 1 12 exit 1
13fi 13fi
14 14
@@ -28,7 +28,7 @@ if ! echo "$permit_root_login" | grep -q "no"; then
28 # Look for an explicitly set AuthorizedKeysFile path 28 # Look for an explicitly set AuthorizedKeysFile path
29 auth_keys_path_line=$(grep -E "^[[:space:]]*AuthorizedKeysFile" /etc/ssh/sshd_config) 29 auth_keys_path_line=$(grep -E "^[[:space:]]*AuthorizedKeysFile" /etc/ssh/sshd_config)
30 30
31 if [ -n "$auth_keys_path_line" ]; then 31 if [[ -n "$auth_keys_path_line" ]]; then
32 # An explicit path is set. Extract the path. 32 # An explicit path is set. Extract the path.
33 # This removes the 'AuthorizedKeysFile' keyword and leading/trailing whitespace. 33 # This removes the 'AuthorizedKeysFile' keyword and leading/trailing whitespace.
34 auth_keys_path=$(echo "$auth_keys_path_line" | awk '{print $2}') 34 auth_keys_path=$(echo "$auth_keys_path_line" | awk '{print $2}')
@@ -45,7 +45,7 @@ if ! echo "$permit_root_login" | grep -q "no"; then
45 fi 45 fi
46 46
47 echo "Checking for file at: $actual_path" 47 echo "Checking for file at: $actual_path"
48 if [ -f "$actual_path" ]; then 48 if [[ -f "$actual_path" ]]; then
49 echo "[CRITICAL] Found authorized keys file for root at $actual_path" 49 echo "[CRITICAL] Found authorized keys file for root at $actual_path"
50 echo "Contents:" 50 echo "Contents:"
51 echo "----------------------------------------" 51 echo "----------------------------------------"
sampling/sample.html +5 −5
@@ -80,7 +80,7 @@ function handleFormSubmit(event) {
80 // Use the custom seed if provided; otherwise draw a strong random seed and 80 // Use the custom seed if provided; otherwise draw a strong random seed and
81 // write it back so the (reproducible) sample can always be tied to a seed. 81 // write it back so the (reproducible) sample can always be tied to a seed.
82 const seed = customSeedInput 82 const seed = customSeedInput
83 ? parseInt(customSeedInput) 83 ? Number.parseInt(customSeedInput)
84 : crypto.getRandomValues(new Uint32Array(1))[0] % 1000000; 84 : crypto.getRandomValues(new Uint32Array(1))[0] % 1000000;
85 if (!customSeedInput) { 85 if (!customSeedInput) {
86 document.getElementById('customSeed').value = seed; 86 document.getElementById('customSeed').value = seed;
@@ -89,16 +89,16 @@ function handleFormSubmit(event) {
89} 89}
90 90
91function generateSamples(seed) { 91function generateSamples(seed) {
92 const populationSize = parseInt(document.getElementById('populationSize').value); 92 const populationSize = Number.parseInt(document.getElementById('populationSize').value);
93 const sampleSize = parseInt(document.getElementById('sampleSize').value); 93 const sampleSize = Number.parseInt(document.getElementById('sampleSize').value);
94 const replacementSize = parseInt(document.getElementById('replacementSize').value || 0); 94 const replacementSize = Number.parseInt(document.getElementById('replacementSize').value || 0);
95 const resultsDiv = document.getElementById('results'); 95 const resultsDiv = document.getElementById('results');
96 96
97 // Clear previous results 97 // Clear previous results
98 resultsDiv.innerHTML = ''; 98 resultsDiv.innerHTML = '';
99 99
100 // Validate inputs 100 // Validate inputs
101 if (isNaN(populationSize) || isNaN(sampleSize) || populationSize <= 0 || sampleSize <= 0) { 101 if (Number.isNaN(populationSize) || Number.isNaN(sampleSize) || populationSize <= 0 || sampleSize <= 0) {
102 alert("Please enter valid numbers for required fields."); 102 alert("Please enter valid numbers for required fields.");
103 return; 103 return;
104 } 104 }
sampling/sampling_tool/cli.py +17 −18
@@ -23,6 +23,11 @@ from .reconciliation import build_reconciliation, build_strata_summary
23from .reporting import RunLogger, build_methodology 23from .reporting import RunLogger, build_methodology
24from .validation import validate_and_prepare 24from .validation import validate_and_prepare
25 25
26# Output filenames, defined once so writer and tracker never drift.
27_POPULATION_VALIDATED_CSV = "population_validated.csv"
28_EXCLUDED_ROWS_CSV = "excluded_rows.csv"
29_DUPLICATE_IDS_CSV = "duplicate_ids.csv"
30
26 31
27def build_parser() -> ArgumentParser: 32def build_parser() -> ArgumentParser:
28 parser = ArgumentParser(description="Generate documented audit samples.") 33 parser = ArgumentParser(description="Generate documented audit samples.")
@@ -211,7 +216,6 @@ def run(options) -> Path:
211 _write_outputs( 216 _write_outputs(
212 run_dir, 217 run_dir,
213 options, 218 options,
214 source,
215 validated, 219 validated,
216 excluded_rows, 220 excluded_rows,
217 duplicate_rows, 221 duplicate_rows,
@@ -227,8 +231,6 @@ def run(options) -> Path:
227 print(f"ERROR: {exc}", file=sys.stderr) 231 print(f"ERROR: {exc}", file=sys.stderr)
228 _write_failure_outputs( 232 _write_failure_outputs(
229 run_dir, 233 run_dir,
230 options,
231 source,
232 filtered, 234 filtered,
233 excluded_rows, 235 excluded_rows,
234 duplicate_rows, 236 duplicate_rows,
@@ -307,7 +309,6 @@ def add_sample_metadata(
307def _write_outputs( 309def _write_outputs(
308 run_dir: Path, 310 run_dir: Path,
309 options, 311 options,
310 source: pd.DataFrame,
311 validated: pd.DataFrame, 312 validated: pd.DataFrame,
312 excluded_rows: pd.DataFrame, 313 excluded_rows: pd.DataFrame,
313 duplicate_rows: pd.DataFrame, 314 duplicate_rows: pd.DataFrame,
@@ -315,17 +316,17 @@ def _write_outputs(
315 strata_rows: list[dict[str, object]], 316 strata_rows: list[dict[str, object]],
316 output_files: list[str], 317 output_files: list[str],
317) -> None: 318) -> None:
318 write_csv(validated, run_dir / "population_validated.csv") 319 write_csv(validated, run_dir / _POPULATION_VALIDATED_CSV)
319 _track(output_files, "population_validated.csv") 320 _track(output_files, _POPULATION_VALIDATED_CSV)
320 if options.method in {"random", "stratified"}: 321 if options.method in {"random", "stratified"}:
321 write_csv(sample, run_dir / "sample.csv") 322 write_csv(sample, run_dir / "sample.csv")
322 _track(output_files, "sample.csv") 323 _track(output_files, "sample.csv")
323 if not excluded_rows.empty: 324 if not excluded_rows.empty:
324 write_csv(excluded_rows, run_dir / "excluded_rows.csv") 325 write_csv(excluded_rows, run_dir / _EXCLUDED_ROWS_CSV)
325 _track(output_files, "excluded_rows.csv") 326 _track(output_files, _EXCLUDED_ROWS_CSV)
326 if not duplicate_rows.empty: 327 if not duplicate_rows.empty:
327 write_csv(duplicate_rows, run_dir / "duplicate_ids.csv") 328 write_csv(duplicate_rows, run_dir / _DUPLICATE_IDS_CSV)
328 _track(output_files, "duplicate_ids.csv") 329 _track(output_files, _DUPLICATE_IDS_CSV)
329 if options.method == "stratified": 330 if options.method == "stratified":
330 write_csv(build_strata_summary(strata_rows), run_dir / "strata_summary.csv") 331 write_csv(build_strata_summary(strata_rows), run_dir / "strata_summary.csv")
331 _track(output_files, "strata_summary.csv") 332 _track(output_files, "strata_summary.csv")
@@ -333,22 +334,20 @@ def _write_outputs(
333 334
334def _write_failure_outputs( 335def _write_failure_outputs(
335 run_dir: Path, 336 run_dir: Path,
336 options,
337 source: pd.DataFrame,
338 filtered: pd.DataFrame, 337 filtered: pd.DataFrame,
339 excluded_rows: pd.DataFrame, 338 excluded_rows: pd.DataFrame,
340 duplicate_rows: pd.DataFrame, 339 duplicate_rows: pd.DataFrame,
341 output_files: list[str], 340 output_files: list[str],
342) -> None: 341) -> None:
343 if not filtered.empty: 342 if not filtered.empty:
344 write_csv(filtered, run_dir / "population_validated.csv") 343 write_csv(filtered, run_dir / _POPULATION_VALIDATED_CSV)
345 _track(output_files, "population_validated.csv") 344 _track(output_files, _POPULATION_VALIDATED_CSV)
346 if not excluded_rows.empty: 345 if not excluded_rows.empty:
347 write_csv(excluded_rows, run_dir / "excluded_rows.csv") 346 write_csv(excluded_rows, run_dir / _EXCLUDED_ROWS_CSV)
348 _track(output_files, "excluded_rows.csv") 347 _track(output_files, _EXCLUDED_ROWS_CSV)
349 if not duplicate_rows.empty: 348 if not duplicate_rows.empty:
350 write_csv(duplicate_rows, run_dir / "duplicate_ids.csv") 349 write_csv(duplicate_rows, run_dir / _DUPLICATE_IDS_CSV)
351 _track(output_files, "duplicate_ids.csv") 350 _track(output_files, _DUPLICATE_IDS_CSV)
352 351
353 352
354def _concat_nonempty(frames: list[pd.DataFrame]) -> pd.DataFrame: 353def _concat_nonempty(frames: list[pd.DataFrame]) -> pd.DataFrame:
sampling/tests/test_validation.py +2 −1
@@ -34,8 +34,9 @@ def test_duplicate_ids_fail_by_default_and_write_duplicate_file(tmp_path):
34 source, index=False 34 source, index=False
35 ) 35 )
36 36
37 options = _options(source, tmp_path / "out")
37 with pytest.raises(AuditSamplingError): 38 with pytest.raises(AuditSamplingError):
38 run(_options(source, tmp_path / "out")) 39 run(options)
39 40
40 run_dir = next((tmp_path / "out").glob("sample_*")) 41 run_dir = next((tmp_path / "out").glob("sample_*"))
41 duplicates = pd.read_csv(run_dir / "duplicate_ids.csv") 42 duplicates = pd.read_csv(run_dir / "duplicate_ids.csv")
tui/tests/test_aws_runner.py +2 −1
@@ -101,7 +101,8 @@ def test_session_build_failure_is_reported(tmp_path, fake_checks, monkeypatch):
101 assert ("error", "AWS session") in kinds 101 assert ("error", "AWS session") in kinds
102 # Run still ends with a summary and writes the (empty) package. 102 # Run still ends with a summary and writes the (empty) package.
103 summary = [e for e in events if e.kind == "summary"] 103 summary = [e for e in events if e.kind == "summary"]
104 assert summary and summary[0].count == 0 104 assert summary
105 assert summary[0].count == 0
105 assert sections == [] 106 assert sections == []
106 assert os.path.exists(tmp_path / "summary.txt") 107 assert os.path.exists(tmp_path / "summary.txt")
107 108