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