Commit cb8640beec
Unsigned
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 --- |
| 11 | if ! command -v aws &> /dev/null || ! command -v jq &> /dev/null; then | 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 | exit 1 | 13 | exit 1 |
| 14 | fi | 14 | fi |
| 15 | 15 | ||
| @@ -19,15 +19,15 @@ echo "Fetching IAM Identity Center and Account details..." | |||
| 19 | INSTANCE_ARN=$(aws sso-admin list-instances --query "Instances[0].InstanceArn" --output text) | 19 | INSTANCE_ARN=$(aws sso-admin list-instances --query "Instances[0].InstanceArn" --output text) |
| 20 | IDENTITY_STORE_ID=$(aws sso-admin list-instances --query "Instances[0].IdentityStoreId" --output text) | 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 | 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." | 23 | echo "Error: Could not find IAM Identity Center instance ARN or Identity Store ID." >&2 |
| 24 | exit 1 | 24 | exit 1 |
| 25 | fi | 25 | fi |
| 26 | 26 | ||
| 27 | ACCOUNT_ID=$(aws organizations list-accounts --query "Accounts[?Name=='$ACCOUNT_NAME' && Status=='ACTIVE'].Id" --output text) | 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 | 29 | if [[ -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 |
| 32 | fi | 32 | fi |
| 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 | ||
| 44 | if [ -z "$PROVISIONED_SETS_ARN" ]; then | 44 | if [[ -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 |
| 47 | fi | 47 | fi |
| @@ -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 "---" | |||
| 18 | echo "1. Retrieving all bucket names..." | 18 | echo "1. Retrieving all bucket names..." |
| 19 | BUCKET_LIST=$(aws s3api list-buckets --region "$MASTER_REGION" --query 'Buckets[].Name' --output text) | 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 | echo "✅ No S3 buckets found in this account." | 22 | echo "✅ No S3 buckets found in this account." |
| 23 | exit 0 | 23 | exit 0 |
| 24 | fi | 24 | fi |
| @@ -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 | ||
| 135 | def privileged_access(org, cfg, repo_collabs): | 135 | def 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 | ||
| 199 | def permission_matrix(org, cfg, repo_collabs): | 199 | def 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 | |||
| 12 | from .api import paginate | 12 | from .api import paginate |
| 13 | 13 | ||
| 14 | 14 | ||
| 15 | def approval_rules(group, cfg, projects): | 15 | def 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 | ||
| 15 | def branch_protections(group, cfg, projects): | 15 | def 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 | ||
| 47 | def project_members(group, cfg, projects): | 47 | def 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 | |||
| 7 | from .api import paginate | 7 | from .api import paginate |
| 8 | 8 | ||
| 9 | 9 | ||
| 10 | def pipelines(group, cfg, projects): | 10 | def 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 | ||
| 20 | def project_list(group, cfg, projects): | 20 | def 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 |
| 6 | import pandas as pd | 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 | # Load the data into a pandas DataFrame | 13 | # Load the data into a pandas DataFrame |
| 9 | df_input = pd.read_csv("./data.csv") | 14 | df_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 @@ | |||
| 4 | extract_password_params() { | 4 | extract_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 |
| 50 | analyze_login_defs() { | 51 | analyze_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 |
| 8 | log_section() { | 8 | log_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 |
| 33 | log_command_output() { | 37 | log_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 |
| 4 | if [ "$EUID" -ne 0 ]; then | 4 | if [[ "$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 |
| 7 | fi | 7 | fi |
| 8 | 8 | ||
| 9 | # Check if the sshd_config file exists | 9 | # Check if the sshd_config file exists |
| 10 | if [ ! -f /etc/ssh/sshd_config ]; then | 10 | if [[ ! -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 |
| 13 | fi | 13 | fi |
| 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 | ||
| 91 | function generateSamples(seed) { | 91 | function 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 | |||
| 23 | from .reporting import RunLogger, build_methodology | 23 | from .reporting import RunLogger, build_methodology |
| 24 | from .validation import validate_and_prepare | 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 | def build_parser() -> ArgumentParser: | 32 | def 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( | |||
| 307 | def _write_outputs( | 309 | def _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 | ||
| 334 | def _write_failure_outputs( | 335 | def _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 | ||
| 354 | def _concat_nonempty(frames: list[pd.DataFrame]) -> pd.DataFrame: | 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 | 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 | ||