diff --git a/.github/workflows/README.md b/.github/workflows/README.md
index f6b1a440e175..4a32aa271df1 100644
--- a/.github/workflows/README.md
+++ b/.github/workflows/README.md
@@ -545,4 +545,3 @@ PostCommit Jobs run in a schedule against master branch and generally do not get
| [ Infrastructure Policy Enforcer ](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_PolicyEnforcer.yml) | N/A | [](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_PolicyEnforcer.yml?query=event%3Aschedule) |
| [ Modify the GCP User Roles according to the infra/users.yml file ](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_UsersPermissions.yml) | N/A | [](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_UsersPermissions.yml?query=event%3Aschedule) |
| [ Service Account Keys Management ](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_ServiceAccountKeys.yml) | N/A | [](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_ServiceAccountKeys.yml?query=event%3Aschedule) |
-| [ Unmanaged Service Accounts Keys Audit ](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml) | N/A | [](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml?query=event%3Aschedule) |
diff --git a/.github/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml b/.github/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml
deleted file mode 100644
index 7efb81229364..000000000000
--- a/.github/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml
+++ /dev/null
@@ -1,69 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-# This workflow works with the GCP security log analyzer to
-# generate weekly security reports and initialize log sinks
-
-name: Unmanaged Service Accounts Keys Audit
-
-on:
- workflow_dispatch:
- schedule:
- # Every day at 00:00 UTC
- - cron: '0 0 * * *'
-
-concurrency:
- group: ${{ github.workflow }}
- cancel-in-progress: false
-
-permissions:
- contents: read
- issues: write
- id-token: write
-
-jobs:
- beam_UnmanagedKeysAudit:
- name: Audit Unmanaged Service Account Keys
- runs-on: [self-hosted, ubuntu-24.04, main]
- timeout-minutes: 30
- steps:
- - uses: actions/checkout@v7
-
- - name: Setup gcloud
- uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
-
- - name: Setup Python
- uses: actions/setup-python@v4
- with:
- python-version: '3.13'
-
- - name: Install Python dependencies
- working-directory: ./infra/enforcement
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt
-
- - name: Run Unmanaged Service Account Keys Audit
- working-directory: ./infra/enforcement
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- GITHUB_REPOSITORY: ${{ github.repository }}
- run: python account_keys.py --action announce
-
-
-
-
diff --git a/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml b/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml
index cfef684c6f9f..f94a4d0554eb 100644
--- a/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml
+++ b/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml
@@ -24,7 +24,7 @@ on:
workflow_dispatch:
schedule:
# Once a week at 9:00 AM on Monday
- - cron: '0 9 * * 1'
+ - cron: '0 0 * * *'
# This allows a subsequently queued workflow run to interrupt previous runs
concurrency:
@@ -35,6 +35,7 @@ concurrency:
permissions:
contents: read
issues: write
+ id-token: write
jobs:
beam_Infrastructure_PolicyEnforcer:
@@ -43,21 +44,21 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
-
+
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.13'
-
+
- name: Install Python dependencies
working-directory: ./infra/enforcement
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
-
+
- name: Setup gcloud
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
-
+
- name: Run IAM Policy Enforcement
working-directory: ./infra/enforcement
env:
@@ -68,7 +69,7 @@ jobs:
EMAIL_ADDRESS: ${{ secrets.ISSUE_REPORT_SENDER_EMAIL_ADDRESS }}
EMAIL_PASSWORD: ${{ secrets.ISSUE_REPORT_SENDER_EMAIL_PASSWORD }}
EMAIL_RECIPIENT: "dev@beam.apache.org"
- run: python iam.py --action print
+ run: python iam.py --action announce
- name: Run Account Keys Policy Enforcement
working-directory: ./infra/enforcement
@@ -80,4 +81,4 @@ jobs:
EMAIL_ADDRESS: ${{ secrets.ISSUE_REPORT_SENDER_EMAIL_ADDRESS }}
EMAIL_PASSWORD: ${{ secrets.ISSUE_REPORT_SENDER_EMAIL_PASSWORD }}
EMAIL_RECIPIENT: "dev@beam.apache.org"
- run: python account_keys.py --action print
+ run: python account_keys.py --action announce
diff --git a/infra/enforcement/README.md b/infra/enforcement/README.md
index 6d883f7e6806..b92e5f7e1802 100644
--- a/infra/enforcement/README.md
+++ b/infra/enforcement/README.md
@@ -134,16 +134,18 @@ The enforcement tools are integrated with GitHub Actions to provide automated co
### Workflow Configuration
-The repository includes workflows for different security domains:
-- **IAM Policy Enforcer** (`.github/workflows/beam_Infrastructure_PolicyEnforcer.yml`): Runs weekly on Mondays at 9:00 AM UTC.
-- **Unmanaged Keys Audit** (`.github/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml`): Runs daily at 00:00 UTC. It manages the continuous execution of the `account_keys.py` script to swiftly detect rogue service account keys generated outside the official rotation system.
-- **Manual trigger**: Can be triggered manually via `workflow_dispatch`
-- **Actions**: Runs both IAM and Account Keys enforcement with the `announce` action
+The enforcement tools are consolidated into a single daily workflow (`.github/workflows/beam_Infrastructure_PolicyEnforcer.yml`) that runs automatically at 00:00 UTC.
+
+This unified workflow executes both security domains sequentially:
+- **IAM Policy Enforcement:** Validates user bindings against the defined policies.
+- **Unmanaged Keys Audit:** Detects rogue service account keys generated outside the official rotation system.
**Note**:
-- The email service is configured to use gmail
-- The recipient email is set to `dev@beam.apache.org` for Apache Beam project notifications
-- The `GITHUB_TOKEN` is automatically provided by GitHub Actions and doesn't need to be configured manually
+- **Manual trigger**: The workflow can also be triggered manually via `workflow_dispatch`.
+- **Actions**: It executes the respective Python scripts using the `announce` action.
+- The email service is configured to use gmail.
+- The recipient email is set to `dev@beam.apache.org` for Apache Beam project notifications.
+- The `GITHUB_TOKEN` is automatically provided by GitHub Actions and doesn't need to be configured manually.
## Account Keys
diff --git a/infra/enforcement/account_keys.py b/infra/enforcement/account_keys.py
index 31c1354319d6..b9719fc722ec 100644
--- a/infra/enforcement/account_keys.py
+++ b/infra/enforcement/account_keys.py
@@ -19,6 +19,7 @@
import yaml
import argparse
import os
+from datetime import datetime, timezone
from typing import List, Dict, TypedDict, Optional
from google.cloud import secretmanager
from google.cloud import iam_admin_v1
@@ -315,18 +316,17 @@ def check_compliance(self) -> List[str]:
msg = f"Service account '{service_account}' is not declared in the service account keys file."
compliance_issues.append(msg)
self.logger.warning(msg)
- else:
iam_keys = self._get_user_managed_keys_from_iam(service_account)
- if iam_keys:
- secret_name = f"{self._denormalize_account_email(service_account)}-key"
- legal_keys = []
- if secret_name in managed_secrets:
- legal_keys = self._get_verified_keys_from_secret_manager(secret_name)
- unmanaged_keys = set(iam_keys) - set(legal_keys)
- for unmanaged_key in unmanaged_keys:
- msg = f"SECURITY ALERT: Unmanaged key '{unmanaged_key}' detected on account '{service_account}'. This key was created outside of Beam's service account management system. "
- compliance_issues.append(msg)
- self.logger.warning(msg)
+ if iam_keys:
+ secret_name = f"{self._denormalize_account_email(service_account)}-key"
+ legal_keys = []
+ if secret_name in managed_secrets:
+ legal_keys = self._get_verified_keys_from_secret_manager(secret_name)
+ unmanaged_keys = set(iam_keys) - set(legal_keys)
+ for unmanaged_key in unmanaged_keys:
+ msg = f"SECURITY ALERT: Unmanaged key '{unmanaged_key}' detected on account '{service_account}'. This key was created outside of Beam's service account management system. "
+ compliance_issues.append(msg)
+ self.logger.warning(msg)
extracted_secrets = [f"{self._denormalize_account_email(account['account_id'])}-key" for account in file_service_accounts]
@@ -380,8 +380,9 @@ def create_announcement(self, recipient: str) -> None:
if general_issues:
self.logger.info(f"Found {len(general_issues)} general compliance issues. Triggering announcement...")
- title = f"Account Keys Compliance Issue Detected"
- body = f"Account keys for project {self.project_id} are not compliant with the defined policies on {self.service_account_keys_file}\n\n"
+ title = f"[SECURITY] Action Required: Unauthorized Service Accounts Detected"
+ body = f"Unauthorized Service Accounts Report\n\n"
+ body += f"Account keys for project {self.project_id} are not compliant with the defined policies on {self.service_account_keys_file}\n\n"
for issue in general_issues:
body += f"- {issue}\n"
@@ -405,23 +406,44 @@ def print_announcement(self, recipient: str) -> None:
"""
if not self.sending_client:
raise ValueError("SendingClient is required for printing announcements")
-
+
diff = self.check_compliance()
if not diff:
self.logger.info("No compliance issues found, no announcement will be printed.")
return
- title = f"Account Keys Compliance Issue Detected"
- body = f"Account keys for project {self.project_id} are not compliant with the defined policies on {self.service_account_keys_file}\n\n"
- for issue in diff:
- body += f"- {issue}\n"
+ unmanaged_keys_issues = [issue for issue in diff if "SECURITY ALERT" in issue]
+ general_issues = [issue for issue in diff if "SECURITY ALERT" not in issue]
+
+ if general_issues:
+ self.logger.info("Printing general compliance announcement...")
+ title = f"[SECURITY] Action Required: Unauthorized Service Accounts Detected"
+ body = f"Unauthorized Service Accounts Report\n\n"
+ body += f"Account keys for project {self.project_id} are not compliant with the defined policies on {self.service_account_keys_file}\n\n"
+ for issue in general_issues:
+ body += f"- {issue}\n"
+
+ announcement = f"Dear team,\n\nThis is an automated notification about compliance issues detected in the Account Keys policy for project {self.project_id}.\n\n"
+ announcement += f"We found {len(general_issues)} compliance issue(s) that need your attention.\n"
+ announcement += f"\nPlease check the GitHub issue for detailed information and take appropriate action to resolve these compliance violations."
- announcement = f"Dear team,\n\nThis is an automated notification about compliance issues detected in the Account Keys policy for project {self.project_id}.\n\n"
- announcement += f"We found {len(diff)} compliance issue(s) that need your attention.\n"
- announcement += f"\nPlease check the GitHub issue for detailed information and take appropriate action to resolve these compliance violations."
+ self.sending_client.print_announcement(title, body, recipient, announcement)
- self.sending_client.print_announcement(title, body, recipient, announcement)
+ if unmanaged_keys_issues:
+ self.logger.info("Printing security dashboard update for unmanaged keys...")
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
+ print("\n" + "="*60)
+ print("SIMULATING GITHUB SECURITY ISSUE CREATION/UPDATE")
+ print("="*60)
+ print("Title: [SECURITY] Action Required: Unmanaged Service Account Keys Detected\n")
+ print(f"Body:\n### Unmanaged Keys Audit Report ({timestamp})")
+ print(f"The following unauthorized or unmanaged keys were detected in `{self.project_id}`:\n")
+ for issue in unmanaged_keys_issues:
+ print(f"- {issue}")
+ print("\n*Please investigate and revoke these keys if they are not part of the official rotation system.*\n")
+ print("### History\n\nClick to expand
\n\n[... Previous reports would be collapsed here ...]\n ")
+ print("="*60 + "\n")
def generate_compliance(self) -> None:
"""
diff --git a/infra/enforcement/iam.py b/infra/enforcement/iam.py
index c4c65c7c679c..3d86d0b9b6ee 100644
--- a/infra/enforcement/iam.py
+++ b/infra/enforcement/iam.py
@@ -21,8 +21,9 @@
import yaml
from google.api_core import exceptions
from google.cloud import resourcemanager_v3
-from typing import Optional, List, Dict, Tuple
+from typing import Optional, List, Dict, tuple
from sending import SendingClient
+from datetime import datetime, timezone
CONFIG_FILE = "config.yml"
@@ -214,7 +215,7 @@ def check_compliance(self) -> List[str]:
self.logger.info(error_msg)
raise RuntimeError(error_msg)
- differences = []
+ differences = []
all_emails = set(current_users.keys()) | set(existing_users.keys())
@@ -223,7 +224,7 @@ def check_compliance(self) -> List[str]:
existing_user = existing_users.get(email)
if current_user and not existing_user:
- differences.append(f"User {email} not found in existing policy.")
+ differences.append(f"SECURITY ALERT: Unauthorized user '{email}' detected in GCP but not found in existing policy.")
elif not current_user and existing_user:
differences.append(f"User {email} found in policy file but not in GCP.")
elif current_user and existing_user:
@@ -247,51 +248,86 @@ def create_announcement(self, recipient: str) -> None:
"""
if not self.sending_client:
raise ValueError("SendingClient is required for creating announcements")
-
diff = self.check_compliance()
if not diff:
self.logger.info("No compliance issues found, no announcement will be created.")
return
- title = f"IAM Policy Non-Compliance Detected"
- body = f"IAM policy for project {self.project_id} is not compliant with the defined policies on {self.users_file}\n\n"
- for issue in diff:
- body += f"- {issue}\n"
+ security_alerts = [issue for issue in diff if "SECURITY ALERT" in issue]
+ general_issues = [issue for issue in diff if "SECURITY ALERT" not in issue]
+
+ if general_issues:
+ self.logger.info(f"Found {len(general_issues)} general IAM compliance issues. Triggering announcement...")
+ title = f"IAM Policy Non-Compliance Detected"
+ body = f"IAM policy for project {self.project_id} is not compliant with the defined policies on {self.users_file}\n\n"
+ for issue in general_issues:
+ body += f"- {issue}\n"
+
+ announcement = f"Dear team,\n\nThis is an automated notification about compliance issues detected in the IAM policy for project {self.project_id}.\n\n"
+ announcement += f"We found {len(general_issues)} compliance issue(s) that need your attention.\n"
+ announcement += f"\nPlease check the GitHub issue for detailed information and take appropriate action to resolve these compliance violations."
+
+ self.sending_client.create_announcement(title, body, recipient, announcement)
- announcement = f"Dear team,\n\nThis is an automated notification about compliance issues detected in the IAM policy for project {self.project_id}.\n\n"
- announcement += f"We found {len(diff)} compliance issue(s) that need your attention.\n"
- announcement += f"\nPlease check the GitHub issue for detailed information and take appropriate action to resolve these compliance violations."
+ if security_alerts:
+ self.logger.info(f"Found {len(security_alerts)} critical IAM security alerts. Dispatching to GitHub security issue...")
+ title = f"[SECURITY] Action Required: Unauthorized IAM Users Detected"
+ body = f"Critical security violations detected in IAM policies for project {self.project_id}:\n\n"
+ for issue in security_alerts:
+ body += f"- {issue}\n"
- self.sending_client.create_announcement(title, body, recipient, announcement)
+ announcement = f"URGENT: Dear team,\n\nThis is an automated security alert regarding unauthorized IAM access in project {self.project_id}.\n\n"
+ announcement += f"We found {len(security_alerts)} critical security alert(s) that require IMMEDIATE attention.\n"
+ announcement += f"\nPlease check the GitHub issue for detailed information and revoke unauthorized access immediately."
+
+ self.sending_client.create_announcement(title, body, recipient, announcement)
def print_announcement(self, recipient: str) -> None:
"""
Prints announcement details instead of sending them (for testing purposes).
-
+
Args:
recipient (str): The email address of the announcement recipient.
"""
if not self.sending_client:
raise ValueError("SendingClient is required for printing announcements")
-
+
diff = self.check_compliance()
if not diff:
self.logger.info("No compliance issues found, no announcement will be printed.")
return
- title = f"IAM Policy Non-Compliance Detected"
- body = f"IAM policy for project {self.project_id} is not compliant with the defined policies on {self.users_file}\n\n"
- for issue in diff:
- body += f"- {issue}\n"
+ security_alerts = [issue for issue in diff if "SECURITY ALERT" in issue]
+ general_issues = [issue for issue in diff if "SECURITY ALERT" not in issue]
- announcement = f"Dear team,\n\nThis is an automated notification about compliance issues detected in the IAM policy for project {self.project_id}.\n\n"
- announcement += f"We found {len(diff)} compliance issue(s) that need your attention.\n"
- announcement += f"\nPlease check the GitHub issue for detailed information and take appropriate action to resolve these compliance violations."
+ if general_issues:
+ self.logger.info(f"Found {len(general_issues)} general IAM compliance issues. Printing announcement...")
+ title = f"IAM Policy Non-Compliance Detected"
+ body = f"IAM policy for project {self.project_id} is not compliant with the defined policies on {self.users_file}\n\n"
+ for issue in general_issues:
+ body += f"- {issue}\n"
+
+ announcement = f"Dear team,\n\nThis is an automated notification about compliance issues detected in the IAM policy for project {self.project_id}.\n\n"
+ announcement += f"We found {len(general_issues)} compliance issue(s) that need your attention.\n"
+ announcement += f"\nPlease check the GitHub issue for detailed information and take appropriate action to resolve these compliance violations."
+
+ self.sending_client.print_announcement(title, body, recipient, announcement)
+
+ if security_alerts:
+ self.logger.info("Printing security dashboard update for IAM vulnerabilities...")
+ title = f"[SECURITY] Action Required: Unauthorized IAM Users Detected"
+ body = f"Critical security violations detected in IAM policies for project {self.project_id}:\n\n"
+ for issue in security_alerts:
+ body += f"- {issue}\n"
+
+ announcement = f"URGENT: Dear team,\n\nThis is an automated security alert regarding unauthorized IAM access in project {self.project_id}.\n\n"
+ announcement += f"We found {len(security_alerts)} critical security alert(s) that require IMMEDIATE attention.\n"
+ announcement += f"\nPlease check the GitHub issue for detailed information and revoke unauthorized access immediately."
+
+ self.sending_client.print_announcement(title, body, recipient, announcement)
- self.sending_client.print_announcement(title, body, recipient, announcement)
-
def generate_compliance(self) -> None:
"""
Modifies the users file to match the current IAM policy.
diff --git a/infra/enforcement/sending.py b/infra/enforcement/sending.py
index 37de025a207f..0b85d09cae72 100644
--- a/infra/enforcement/sending.py
+++ b/infra/enforcement/sending.py
@@ -18,6 +18,7 @@
import smtplib, ssl
from typing import List, Optional
from dataclasses import dataclass
+from datetime import datetime, timezone
@dataclass
class GitHubIssue:
@@ -183,7 +184,7 @@ def report_unmanaged_keys(self, project_id: str, compilance_issues: List[str]) -
issue_title = "[SECURITY] Action Required: Unmanaged Service Account Keys Detected"
#markdown body
- timestamp = __import__("datetime").datetime.now(__import__("datetime").timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
new_report = f"### Unmanaged Keys Audit Report ({timestamp})\n"
new_report += f"The following unauthorized or unmanaged keys were detected in `{project_id}`:\n\n"
@@ -202,10 +203,13 @@ def report_unmanaged_keys(self, project_id: str, compilance_issues: List[str]) -
if history_marker in old_body:
# If history already exists, append the new report to it
- headed = old_body.split(history_marker)
+ headed = old_body.split(history_marker, 1)
last_report = headed[0].strip()
old_history = headed[1].replace("", "").strip()
+ if old_history.endswith(""):
+ old_history = old_history[:-10].rstrip()
+
combined_history = f"{last_report}\n\n---\n\n{old_history}"
else:
combined_history = old_body.strip()
@@ -248,18 +252,40 @@ def create_announcement(self, title: str, body: str, recipient: str, announcemen
"""
open_issues = self._get_open_issues(title)
open_issues.sort(key=lambda x: x.updated_at, reverse=True)
+
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
+ new_report = f"### Compliance Audit Report ({timestamp})\n{body}"
+
if open_issues:
- self.logger.info(f"Issue with title '{title}' already exists: #{open_issues[0].number}")
- announcement += f"\n\nRelated GitHub Issue: {open_issues[0].html_url}"
+ target_issue = open_issues[0]
+ self.logger.info(f"Issue with title '{title}' already exists: #{target_issue.number}")
+ announcement += f"\n\nRelated GitHub Issue: {target_issue.html_url}"
+
+ old_body = target_issue.body or ""
+ history_marker = "### History\n\nClick to expand
\n\n"
- if open_issues[0].body != body:
- self.logger.info(f"Updating body of issue #{open_issues[0].number}")
- self.update_issue_body(open_issues[0].number, body)
+ if history_marker in old_body:
+ # If history already exists, append the new report to it
+ headed = old_body.split(history_marker, 1)
+ last_report = headed[0].strip()
+ old_history = headed[1].rstrip()
+
+ if old_history.endswith(" "):
+ old_history = old_history[:-10].rstrip()
+
+ combined_history = f"{last_report}\n\n---\n\n{old_history}"
else:
- self.logger.info(f"No changes detected for issue #{open_issues[0].number}")
+ # First time updating, turn the entire old body into history
+ combined_history = old_body.strip()
+
+ final_body = f"{new_report}\n\n{history_marker}{combined_history}\n"
+
+ self.logger.info(f"Appending report and archiving history to existing issue #{target_issue.number}")
+ self.update_issue_body(target_issue.number, final_body)
self._send_email(title, announcement, recipient)
else:
- new_issue = self.create_issue(title, body)
+ self.logger.info(f"Creating new compliance issue for: {title}")
+ new_issue = self.create_issue(title, new_report)
announcement += f"\n\nRelated GitHub Issue: {new_issue.html_url}"
self._send_email(title, announcement, recipient)
@@ -273,6 +299,13 @@ def print_announcement(self, title: str, body: str, recipient: str, announcement
print(f"Recipient: {recipient}")
print(f"Announcement: {announcement}")
- print("\nSimulating GitHub issue creation...")
- print(f"Title: {title}")
- print(f"Body: {body}")
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
+
+ print("\n" + "="*60)
+ print("SIMULATING GITHUB GENERAL ISSUE CREATION/UPDATE")
+ print("="*60)
+ print(f"Title: {title}\n")
+ print(f"Body:\n### Compliance Audit Report ({timestamp})")
+ print(body)
+ print("### History\n\nClick to expand
\n\n[... Previous reports would be collapsed here ...]\n ")
+ print("="*60 + "\n")
\ No newline at end of file