mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-11 21:54:56 +03:00
301 lines
10 KiB
Python
301 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Cloud storage forensic acquisition agent.
|
|
|
|
Acquires forensic copies of cloud storage objects from AWS S3, Azure Blob
|
|
Storage, and GCP Cloud Storage with integrity verification using SHA-256
|
|
hashes, metadata preservation, and chain-of-custody logging.
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
|
|
try:
|
|
import boto3
|
|
from botocore.exceptions import ClientError
|
|
HAS_BOTO3 = True
|
|
except ImportError:
|
|
HAS_BOTO3 = False
|
|
|
|
|
|
def acquire_s3_objects(bucket, prefix="", output_dir=".", profile=None, region=None):
|
|
"""Acquire S3 objects with forensic integrity verification."""
|
|
if not HAS_BOTO3:
|
|
print("[!] boto3 required: pip install boto3", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
kwargs = {}
|
|
if profile:
|
|
kwargs["profile_name"] = profile
|
|
if region:
|
|
kwargs["region_name"] = region
|
|
session = boto3.Session(**kwargs)
|
|
s3 = session.client("s3")
|
|
|
|
print(f"[*] Acquiring objects from s3://{bucket}/{prefix}")
|
|
evidence_log = []
|
|
|
|
# List objects
|
|
paginator = s3.get_paginator("list_objects_v2")
|
|
pages = paginator.paginate(Bucket=bucket, Prefix=prefix)
|
|
|
|
total_objects = 0
|
|
total_bytes = 0
|
|
|
|
for page in pages:
|
|
for obj in page.get("Contents", []):
|
|
key = obj["Key"]
|
|
size = obj["Size"]
|
|
if key.endswith("/"):
|
|
continue
|
|
|
|
total_objects += 1
|
|
local_path = os.path.join(output_dir, key.replace("/", os.sep))
|
|
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
|
|
|
# Get object metadata
|
|
try:
|
|
head = s3.head_object(Bucket=bucket, Key=key)
|
|
metadata = {
|
|
"content_type": head.get("ContentType", ""),
|
|
"last_modified": head.get("LastModified", "").isoformat()
|
|
if hasattr(head.get("LastModified", ""), "isoformat")
|
|
else str(head.get("LastModified", "")),
|
|
"etag": head.get("ETag", "").strip('"'),
|
|
"version_id": head.get("VersionId", ""),
|
|
"server_side_encryption": head.get("ServerSideEncryption", ""),
|
|
"storage_class": head.get("StorageClass", "STANDARD"),
|
|
"user_metadata": head.get("Metadata", {}),
|
|
}
|
|
except ClientError as e:
|
|
metadata = {"error": str(e)}
|
|
|
|
# Download with hash computation
|
|
sha256 = hashlib.sha256()
|
|
try:
|
|
s3.download_file(bucket, key, local_path)
|
|
with open(local_path, "rb") as f:
|
|
while True:
|
|
chunk = f.read(8192)
|
|
if not chunk:
|
|
break
|
|
sha256.update(chunk)
|
|
file_hash = sha256.hexdigest()
|
|
total_bytes += size
|
|
|
|
entry = {
|
|
"source": f"s3://{bucket}/{key}",
|
|
"local_path": local_path,
|
|
"size": size,
|
|
"sha256": file_hash,
|
|
"metadata": metadata,
|
|
"acquired_at": datetime.now(timezone.utc).isoformat(),
|
|
"status": "OK",
|
|
}
|
|
print(f" [{total_objects:4d}] {key} ({size} bytes, SHA256: {file_hash[:16]}...)")
|
|
except ClientError as e:
|
|
entry = {
|
|
"source": f"s3://{bucket}/{key}",
|
|
"status": "FAIL",
|
|
"error": str(e),
|
|
"acquired_at": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
print(f" [FAIL] {key}: {e}")
|
|
|
|
evidence_log.append(entry)
|
|
|
|
print(f"[+] Acquired {total_objects} objects ({total_bytes / 1024 / 1024:.2f} MB)")
|
|
return evidence_log
|
|
|
|
|
|
def acquire_s3_versions(bucket, key, output_dir=".", profile=None, region=None):
|
|
"""Acquire all versions of a specific S3 object."""
|
|
if not HAS_BOTO3:
|
|
print("[!] boto3 required", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
kwargs = {}
|
|
if profile:
|
|
kwargs["profile_name"] = profile
|
|
if region:
|
|
kwargs["region_name"] = region
|
|
session = boto3.Session(**kwargs)
|
|
s3 = session.client("s3")
|
|
|
|
print(f"[*] Acquiring all versions of s3://{bucket}/{key}")
|
|
evidence_log = []
|
|
|
|
try:
|
|
versions = s3.list_object_versions(Bucket=bucket, Prefix=key)
|
|
except ClientError as e:
|
|
print(f"[!] Error listing versions: {e}", file=sys.stderr)
|
|
return evidence_log
|
|
|
|
for version in versions.get("Versions", []):
|
|
vid = version.get("VersionId", "null")
|
|
size = version.get("Size", 0)
|
|
is_latest = version.get("IsLatest", False)
|
|
|
|
safe_vid = vid.replace("/", "_")[:20]
|
|
base_name = os.path.basename(key)
|
|
local_path = os.path.join(output_dir, f"{base_name}.v_{safe_vid}")
|
|
|
|
try:
|
|
s3.download_file(bucket, key, local_path,
|
|
ExtraArgs={"VersionId": vid} if vid != "null" else {})
|
|
sha256 = hashlib.sha256()
|
|
with open(local_path, "rb") as f:
|
|
while True:
|
|
chunk = f.read(8192)
|
|
if not chunk:
|
|
break
|
|
sha256.update(chunk)
|
|
|
|
entry = {
|
|
"source": f"s3://{bucket}/{key}?versionId={vid}",
|
|
"version_id": vid,
|
|
"is_latest": is_latest,
|
|
"local_path": local_path,
|
|
"size": size,
|
|
"sha256": sha256.hexdigest(),
|
|
"last_modified": str(version.get("LastModified", "")),
|
|
"acquired_at": datetime.now(timezone.utc).isoformat(),
|
|
"status": "OK",
|
|
}
|
|
print(f" Version {vid[:12]:12s} | {size:10d} bytes | "
|
|
f"{'LATEST' if is_latest else ' '} | SHA256: {sha256.hexdigest()[:16]}...")
|
|
except ClientError as e:
|
|
entry = {"source": f"s3://{bucket}/{key}", "version_id": vid,
|
|
"status": "FAIL", "error": str(e)}
|
|
|
|
evidence_log.append(entry)
|
|
|
|
# Also acquire delete markers
|
|
for marker in versions.get("DeleteMarkers", []):
|
|
evidence_log.append({
|
|
"source": f"s3://{bucket}/{key}",
|
|
"version_id": marker.get("VersionId", ""),
|
|
"type": "DELETE_MARKER",
|
|
"last_modified": str(marker.get("LastModified", "")),
|
|
"is_latest": marker.get("IsLatest", False),
|
|
})
|
|
|
|
return evidence_log
|
|
|
|
|
|
def verify_integrity(evidence_log):
|
|
"""Verify SHA-256 hashes of acquired files."""
|
|
print(f"\n[*] Verifying integrity of {len(evidence_log)} acquired objects...")
|
|
verified = 0
|
|
failed = 0
|
|
|
|
for entry in evidence_log:
|
|
if entry.get("status") != "OK" or not entry.get("local_path"):
|
|
continue
|
|
local_path = entry["local_path"]
|
|
expected_hash = entry.get("sha256", "")
|
|
if not os.path.isfile(local_path):
|
|
entry["integrity"] = "MISSING"
|
|
failed += 1
|
|
continue
|
|
|
|
sha256 = hashlib.sha256()
|
|
with open(local_path, "rb") as f:
|
|
while True:
|
|
chunk = f.read(8192)
|
|
if not chunk:
|
|
break
|
|
sha256.update(chunk)
|
|
|
|
if sha256.hexdigest() == expected_hash:
|
|
entry["integrity"] = "VERIFIED"
|
|
verified += 1
|
|
else:
|
|
entry["integrity"] = "MISMATCH"
|
|
failed += 1
|
|
print(f" [FAIL] {local_path}: hash mismatch")
|
|
|
|
print(f"[+] Integrity check: {verified} verified, {failed} failed")
|
|
return verified, failed
|
|
|
|
|
|
def format_summary(evidence_log, verified, failed):
|
|
"""Print acquisition summary."""
|
|
print(f"\n{'='*60}")
|
|
print(f" Cloud Storage Forensic Acquisition Report")
|
|
print(f"{'='*60}")
|
|
ok = sum(1 for e in evidence_log if e.get("status") == "OK")
|
|
err = sum(1 for e in evidence_log if e.get("status") == "FAIL")
|
|
total_bytes = sum(e.get("size", 0) for e in evidence_log if e.get("status") == "OK")
|
|
print(f" Objects Acquired : {ok}")
|
|
print(f" Objects Failed : {err}")
|
|
print(f" Total Size : {total_bytes / 1024 / 1024:.2f} MB")
|
|
print(f" Integrity OK : {verified}")
|
|
print(f" Integrity FAIL : {failed}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Cloud storage forensic acquisition agent"
|
|
)
|
|
sub = parser.add_subparsers(dest="command")
|
|
|
|
p_s3 = sub.add_parser("s3", help="Acquire S3 bucket objects")
|
|
p_s3.add_argument("--bucket", required=True, help="S3 bucket name")
|
|
p_s3.add_argument("--prefix", default="", help="Object key prefix filter")
|
|
p_s3.add_argument("--output-dir", default="./evidence", help="Local output directory")
|
|
|
|
p_ver = sub.add_parser("s3-versions", help="Acquire all versions of S3 object")
|
|
p_ver.add_argument("--bucket", required=True)
|
|
p_ver.add_argument("--key", required=True, help="S3 object key")
|
|
p_ver.add_argument("--output-dir", default="./evidence")
|
|
|
|
parser.add_argument("--profile", help="AWS CLI profile")
|
|
parser.add_argument("--region", help="AWS region")
|
|
parser.add_argument("--skip-verify", action="store_true", help="Skip integrity verification")
|
|
parser.add_argument("--output", "-o", help="Output JSON report path")
|
|
parser.add_argument("--verbose", "-v", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
if not args.command:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
os.makedirs(getattr(args, "output_dir", "./evidence"), exist_ok=True)
|
|
|
|
if args.command == "s3":
|
|
evidence_log = acquire_s3_objects(
|
|
args.bucket, args.prefix, args.output_dir, args.profile, args.region
|
|
)
|
|
elif args.command == "s3-versions":
|
|
evidence_log = acquire_s3_versions(
|
|
args.bucket, args.key, args.output_dir, args.profile, args.region
|
|
)
|
|
|
|
verified, failed = 0, 0
|
|
if not args.skip_verify:
|
|
verified, failed = verify_integrity(evidence_log)
|
|
|
|
format_summary(evidence_log, verified, failed)
|
|
|
|
report = {
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"tool": "Cloud Forensic Acquisition",
|
|
"command": args.command,
|
|
"evidence_log": evidence_log,
|
|
"integrity": {"verified": verified, "failed": failed},
|
|
}
|
|
|
|
if args.output:
|
|
with open(args.output, "w") as f:
|
|
json.dump(report, f, indent=2)
|
|
print(f"\n[+] Report saved to {args.output}")
|
|
elif args.verbose:
|
|
print(json.dumps(report, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|