-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add check-iam and readme for the plugin
- Loading branch information
Showing
4 changed files
with
129 additions
and
325 deletions.
There are no files selected for viewing
55 changes: 55 additions & 0 deletions
55
local-app/python-tools/cross-organization/README.plugin.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # Organization Crawler Module Specification | ||
|
|
||
| This specification defines the contract between the **Master Crawler** (the orchestration engine) and individual **Check Modules**. | ||
|
|
||
| ## **Overview** | ||
|
|
||
| The Crawler is a multi-threaded engine that assumes a role into target accounts and dynamically imports "Check Modules" to gather data. Each module must be a standalone Python file. | ||
|
|
||
| ## **I. Function Signature Requirements** | ||
|
|
||
| Each module MUST implement an `account_task` function with the following signature: | ||
| `def account_task(account_session, account_id, account_name, region):` | ||
|
|
||
| * **`account_session`**: A `boto3.Session` object pre-authenticated into the target account. | ||
| * **`account_id`**: The 12-digit AWS Account ID string. | ||
| * **`account_name`**: The name of the account as defined in AWS Organizations. | ||
| * **`region`**: The default region for the session (though modules may iterate through other regions internally). | ||
|
|
||
| ## **II. Return Data Structure** | ||
|
|
||
| The function must return a dictionary with the following top-level keys: | ||
|
|
||
| 1. **`alias`**: The IAM Account Alias (string). | ||
| 2. **`data`**: A dictionary containing the actual findings. | ||
| * **Resource Keys**: Every entry in `data` should be keyed by a unique identifier (e.g., ARN or Region:ResourceName). | ||
| * **The `resource` Key**: Every inner dictionary MUST contain a `resource` key (usually the ARN). The Crawler uses this for CSV mapping. | ||
|
|
||
|
|
||
| 3. **`account_summary`**: A nested dictionary inside `data` with a `_summary` key used for high-level reporting. | ||
| 4. **`error`**: (Optional) If the task fails, include the exception string here. | ||
|
|
||
| ## **III. Data Standards** | ||
|
|
||
| * **Date Formats**: All datetime objects MUST be converted to strings using `.isoformat()` to ensure JSON serializability. | ||
| * **Booleans**: Convert booleans to strings (`"True"`/`"False"`) if they are going to be displayed in CLI tables or CSVs. | ||
| * **Global vs. Regional**: | ||
| * **Regional Modules**: Should iterate through `ec2.describe_regions()` and gather data for every enabled region. | ||
| * **Global Modules**: (like IAM or Route53) should run exactly once per account to avoid duplicate data. | ||
|
|
||
|
|
||
|
|
||
| ## **IV. File Output Handling** | ||
|
|
||
| Modules do not handle their own file I/O. | ||
|
|
||
| 1. The **Crawler** collects the returned dictionaries from all threads. | ||
| 2. The **Crawler** saves the aggregated list as a JSON file named `audit_results.<module_name>.<timestamp>.json`. | ||
| 3. The **Crawler** automatically flattens the `data` block into a CSV file for any keys that exist in the first returned object. | ||
|
|
||
| ## **V. Development Checklist** | ||
|
|
||
| * [ ] Includes `__version__ = "x.x.x"` at the top. | ||
| * [ ] Uses `get_paginator` for any AWS calls that may return more than 100 items. | ||
| * [ ] Implements a `try/except` block inside `account_task` to prevent one failing account from crashing the entire thread. | ||
| * [ ] Returns "NEVER" or "N/A" for null fields rather than omitting them. |
120 changes: 0 additions & 120 deletions
120
local-app/python-tools/cross-organization/analyze-tag-data.py
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import boto3 | ||
| from datetime import datetime, timezone | ||
|
|
||
| # --- VERSIONING --- | ||
| __version__ = "1.0.0" | ||
|
|
||
| def get_days_ago(dt): | ||
| """Calculates days since a given datetime object.""" | ||
| if not dt: return "NEVER" | ||
| now = datetime.now(timezone.utc) | ||
| return (now - dt).days | ||
|
|
||
| def account_task(account_session, account_id, account_name, region): | ||
| """ | ||
| IAM is a global service; this runs once per account. | ||
| Captures credential aging, MFA status, and key rotation metrics. | ||
| """ | ||
| results = {"alias": "N/A", "data": {}} | ||
| try: | ||
| iam = account_session.client('iam') | ||
| results["alias"] = iam.list_account_aliases().get('AccountAliases', ["N/A"])[0] | ||
|
|
||
| users = [] | ||
| paginator = iam.get_paginator('list_users') | ||
|
|
||
| for page in paginator.paginate(): | ||
| for user in page['Users']: | ||
| username = user['UserName'] | ||
|
|
||
| # Basic Login Stats | ||
| last_login = user.get('PasswordLastUsed') | ||
| days_since_login = get_days_ago(last_login) | ||
|
|
||
| # MFA Status | ||
| mfa = iam.list_mfa_devices(UserName=username).get('MFADevices', []) | ||
| has_mfa = len(mfa) > 0 | ||
|
|
||
| # Access Key Stats | ||
| keys = iam.list_access_keys(UserName=username).get('AccessKeyMetadata', []) | ||
| key_details = [] | ||
| for k in keys: | ||
| k_id = k['AccessKeyId'] | ||
| last_used_resp = iam.get_access_key_last_used(AccessKeyId=k_id) | ||
| last_used_date = last_used_resp.get('AccessKeyLastUsed', {}).get('LastUsedDate') | ||
|
|
||
| key_details.append({ | ||
| "access_key_id": k_id, | ||
| "status": k['Status'], | ||
| "created_date": k['CreateDate'].isoformat(), | ||
| "days_since_rotated": get_days_ago(k['CreateDate']), | ||
| "last_used_date": last_used_date.isoformat() if last_used_date else "NEVER", | ||
| "days_since_used": get_days_ago(last_used_date) | ||
| }) | ||
|
|
||
| user_payload = { | ||
| "resource": user['Arn'], | ||
| "username": username, | ||
| "user_id": user['UserId'], | ||
| "create_date": user['CreateDate'].isoformat(), | ||
| "mfa_enabled": str(has_mfa), | ||
| "last_login_date": last_login.isoformat() if last_login else "NEVER", | ||
| "days_since_login": days_since_login, | ||
| "access_keys": key_details | ||
| } | ||
|
|
||
| # Key into data by Username for the JSON output | ||
| results["data"][username] = user_payload | ||
|
|
||
| results["data"]["account_summary"] = {"_summary": f"USERS:{len(results['data'])}"} | ||
|
|
||
| except Exception as e: | ||
| results["error"] = str(e) | ||
|
|
||
| return results |
Oops, something went wrong.