| import json |
| import re |
| import torch |
| import requests |
| import warnings |
|
|
| warnings.filterwarnings("ignore", category=FutureWarning) |
| warnings.filterwarnings("ignore", message=".*max_new_tokens.*max_length.*") |
|
|
| from unsloth import FastLanguageModel |
|
|
| BASE_URL = "https://kkaustav-cloud-config-auditor.hf.space" |
|
|
| model, tokenizer = FastLanguageModel.from_pretrained( |
| model_name="/content/aws-auditor-sft-v5", |
| max_seq_length=4096, |
| dtype=None, |
| load_in_4bit=True, |
| ) |
| FastLanguageModel.for_inference(model) |
|
|
| |
| SYSTEM_DEFAULT = ( |
| "You are an expert AWS cloud security auditor. " |
| "Return your audit as valid JSON ONLY β no markdown, no code blocks, no extra text.\n" |
| "IMPORTANT: Your ENTIRE response must be ONE complete, closed JSON object. " |
| "Close every array ] and every brace } before you stop.\n" |
| '{"findings":["finding 1"],"severity":["CRITICAL"],' |
| '"recommendations":["fix 1"],"config_patch":{"key":"value"}}' |
| ) |
|
|
| |
| SYSTEM_LAMBDA_IAM = ( |
| "You are an expert AWS Lambda and IAM security auditor for a payment processing service.\n" |
| "Analyze the provided Lambda configuration and report EVERY distinct security misconfiguration.\n\n" |
| "CHECK EACH OF THESE SEPARATELY β each must be its own finding:\n" |
| "1. IAM role: Is AdministratorAccess or any wildcard policy attached? (CRITICAL)\n" |
| "2. Env var DB_PASSWORD: Is a plaintext database password stored here? (CRITICAL)\n" |
| "3. Env var API_SECRET_KEY: Is a plaintext API key stored here? (CRITICAL)\n" |
| "4. Env var STRIPE_SECRET: Is a plaintext Stripe/payment secret stored here? (CRITICAL)\n" |
| "5. FunctionUrl.AuthType: Is it set to NONE (unauthenticated public access)? (HIGH)\n" |
| "6. CORS AllowOrigins: Is it set to ['*'] (wildcard)? (HIGH)\n" |
| "7. VpcConfig: Is it null (Lambda not isolated in a VPC)? (HIGH)\n" |
| "8. ReservedConcurrency: Is it null (no DDoS/cost protection)? (MEDIUM)\n" |
| "9. Tracing.Mode: Is it PassThrough (no X-Ray active tracing)? (LOW)\n" |
| "10. CodeSigningConfigArn: Is it null (no code integrity verification)? (MEDIUM)\n" |
| "11. Runtime: Is it python3.9 (deprecated/end-of-life runtime)? (MEDIUM)\n\n" |
| "Rules:\n" |
| "- List EACH env var secret as a SEPARATE finding β do not group them.\n" |
| "- List ALL findings even if they seem minor.\n" |
| "- Return valid JSON ONLY. No markdown. No code blocks.\n" |
| "- Your ENTIRE response must be ONE complete, closed JSON object.\n" |
| "- severity array must match findings array length.\n" |
| '{"findings":["AdministratorAccess IAM policy attached","DB_PASSWORD stored as plaintext"],' |
| '"severity":["CRITICAL","CRITICAL"],' |
| '"recommendations":["Replace AdministratorAccess with least-privilege policy","Move DB_PASSWORD to AWS Secrets Manager"],' |
| '"config_patch":{' |
| '"ExecutionRole.AttachedPolicies":["arn:aws:iam::123456789012:policy/LambdaPaymentMinimalPolicy"],' |
| '"EnvironmentVariables.DB_PASSWORD":"{{resolve:secretsmanager:payment-db-secret}}",' |
| '"FunctionUrl.AuthType":"AWS_IAM",' |
| '"VpcConfig":{"SubnetIds":["subnet-xxx"],"SecurityGroupIds":["sg-xxx"]},' |
| '"ReservedConcurrency":100,' |
| '"Tracing":{"Mode":"Active"},' |
| '"CodeSigningConfigArn":"arn:aws:lambda:ap-south-1:123456789012:code-signing-config:csc-xxx"' |
| "}}" |
| ) |
|
|
| |
| |
| |
| SYSTEM_RDS_CLOUDTRAIL = SYSTEM_DEFAULT |
|
|
| |
| |
| |
| |
| SYSTEM_IAM_VPC = ( |
| "You are an expert AWS IAM and VPC security auditor.\n" |
| "The config has TWO sections: IAMRole and VPC. You MUST audit BOTH sections.\n" |
| "Report EVERY misconfiguration as a SEPARATE finding.\n\n" |
| "=== SECTION 1: IAMRole findings β check each separately ===\n" |
| "1. AssumeRolePolicyDocument has Principal.Service='*' β any service can assume this role (CRITICAL)\n" |
| "2. InlinePolicies has Action='*' β wildcard action grants full AWS access (CRITICAL)\n" |
| "3. InlinePolicies has Resource='*' β no resource-level restriction (CRITICAL)\n" |
| "4. Inline policy 'AllAccess-temp' used β should be a least-privilege managed policy (MEDIUM)\n" |
| "5. MFAEnabled=false β no MFA requirement on the role (HIGH)\n" |
| "6. PasswordPolicy.MinimumPasswordLength=6 β minimum must be at least 14 (MEDIUM)\n" |
| "7. PasswordPolicy.RequireUppercaseCharacters=false β uppercase not enforced (MEDIUM)\n" |
| "8. PasswordPolicy.RequireNumbers=false β numbers not required (MEDIUM)\n" |
| "9. PasswordPolicy.RequireSymbols=false β symbols not required (MEDIUM)\n" |
| "10. PasswordPolicy.MaxPasswordAge=0 β passwords never expire (MEDIUM)\n" |
| "11. PasswordPolicy.PasswordReusePrevention=0 β old passwords can be reused (MEDIUM)\n\n" |
| "=== SECTION 2: VPC findings β check each separately ===\n" |
| "12. VPC.FlowLogsEnabled=false β VPC Flow Logs are disabled, no traffic visibility (HIGH)\n" |
| "13. VPC.CloudTrailEnabled=false β CloudTrail is disabled, no API audit logging (CRITICAL)\n" |
| "14. VPC.GuardDutyEnabled=false β GuardDuty threat detection is not enabled (HIGH)\n" |
| "15. NetworkACL inbound rule: Protocol='-1', CidrBlock='0.0.0.0/0', Action='allow' β all inbound traffic permitted (HIGH)\n" |
| "16. NetworkACL outbound rule: Protocol='-1', CidrBlock='0.0.0.0/0', Action='allow' β all outbound traffic permitted (MEDIUM)\n" |
| "17. Subnet MapPublicIpOnLaunch=true β instances get public IPs automatically on launch (HIGH)\n\n" |
| "CRITICAL RULES:\n" |
| "- You MUST include findings from BOTH the IAMRole section AND the VPC section.\n" |
| "- Do NOT stop after IAM findings β continue to VPC findings 12-17.\n" |
| "- Each numbered item = one separate finding string.\n" |
| "- Do NOT group multiple items into one finding.\n" |
| "- Respond with valid JSON ONLY. No markdown. No code blocks.\n" |
| "- Your ENTIRE response must be ONE complete, closed JSON object.\n" |
| "- severity array length must EXACTLY match findings array length.\n\n" |
| "Example output structure (use ALL 17 findings):\n" |
| '{"findings":[' |
| '"IAM role trust policy allows any AWS service to assume it β Principal.Service is wildcard (*)",' |
| '"Inline policy AllAccess-temp grants Action=* β full administrative access",' |
| '"Inline policy AllAccess-temp has Resource=* β no resource-level restriction",' |
| '"Inline policy used instead of least-privilege managed policy (AllAccess-temp)",' |
| '"MFAEnabled=false β IAM role has no MFA requirement",' |
| '"PasswordPolicy MinimumPasswordLength=6 β below the 14-character minimum",' |
| '"PasswordPolicy RequireUppercaseCharacters=false β uppercase not required",' |
| '"PasswordPolicy RequireNumbers=false β numbers not required",' |
| '"PasswordPolicy RequireSymbols=false β symbols not required",' |
| '"PasswordPolicy MaxPasswordAge=0 β passwords never expire",' |
| '"PasswordPolicy PasswordReusePrevention=0 β no password reuse prevention",' |
| '"VPC FlowLogsEnabled=false β no network traffic logging",' |
| '"VPC CloudTrailEnabled=false β no API audit logging",' |
| '"VPC GuardDutyEnabled=false β no threat detection",' |
| '"NetworkACL inbound allows all traffic (Protocol=-1, 0.0.0.0/0)",' |
| '"NetworkACL outbound allows all traffic (Protocol=-1, 0.0.0.0/0)",' |
| '"Subnet MapPublicIpOnLaunch=true β public IPs auto-assigned on launch"' |
| "]," |
| '"severity":["CRITICAL","CRITICAL","CRITICAL","MEDIUM","HIGH","MEDIUM","MEDIUM","MEDIUM","MEDIUM","MEDIUM","MEDIUM","HIGH","CRITICAL","HIGH","HIGH","MEDIUM","HIGH"],' |
| '"recommendations":[' |
| '"Restrict Principal.Service to ecs-tasks.amazonaws.com",' |
| '"Replace wildcard Action with specific required permissions",' |
| '"Replace wildcard Resource with specific ARNs",' |
| '"Replace inline policy with least-privilege managed policy",' |
| '"Enable MFA for all IAM role assumptions",' |
| '"Set MinimumPasswordLength to 14",' |
| '"Enable RequireUppercaseCharacters",' |
| '"Enable RequireNumbers",' |
| '"Enable RequireSymbols",' |
| '"Set MaxPasswordAge to 90 days",' |
| '"Set PasswordReusePrevention to 24",' |
| '"Enable VPC Flow Logs",' |
| '"Enable CloudTrail in all regions",' |
| '"Enable GuardDuty",' |
| '"Restrict NACL inbound to known CIDR blocks",' |
| '"Restrict NACL outbound to required destinations",' |
| '"Disable MapPublicIpOnLaunch on all production subnets"' |
| "]," |
| '"config_patch":{' |
| '"IAMRole.AssumeRolePolicyDocument.Statement[0].Principal":{"Service":"ecs-tasks.amazonaws.com"},' |
| '"VPC.FlowLogsEnabled":true,' |
| '"VPC.GuardDutyEnabled":true,' |
| '"VPC.CloudTrailEnabled":true,' |
| '"Subnets[0].MapPublicIpOnLaunch":false' |
| "}}" |
| ) |
|
|
| SYSTEM_MAP = { |
| "medium_lambda_iam": SYSTEM_LAMBDA_IAM, |
| "hard_rds_cloudtrail": SYSTEM_RDS_CLOUDTRAIL, |
| "hard_iam_vpc": SYSTEM_IAM_VPC, |
| } |
|
|
| |
| def repair_json(text): |
| text = text.strip() |
| text = re.sub(r"^```(?:json)?\s*", "", text) |
| text = re.sub(r"\s*```$", "", text) |
| text = text.strip() |
|
|
| try: |
| return json.loads(text) |
| except json.JSONDecodeError: |
| pass |
|
|
| start = text.find("{") |
| if start == -1: |
| return None |
| text = text[start:] |
|
|
| in_string = False |
| escape_next = False |
| stack = [] |
|
|
| for ch in text: |
| if escape_next: |
| escape_next = False |
| continue |
| if ch == "\\" and in_string: |
| escape_next = True |
| continue |
| if ch == '"': |
| in_string = not in_string |
| continue |
| if in_string: |
| continue |
| if ch in "{[": |
| stack.append(ch) |
| elif ch == "]" and stack and stack[-1] == "[": |
| stack.pop() |
| elif ch == "}" and stack and stack[-1] == "{": |
| stack.pop() |
|
|
| repair = "" |
| if in_string: |
| repair += '"' |
| for opener in reversed(stack): |
| repair += "]" if opener == "[" else "}" |
|
|
| try: |
| return json.loads(text + repair) |
| except json.JSONDecodeError: |
| pass |
|
|
| findings_match = re.findall(r'"findings"\s*:\s*\[([^\]]*)', text + repair, re.DOTALL) |
| if findings_match: |
| items = re.findall(r'"((?:[^"\\]|\\.)*)"', findings_match[0]) |
| if items: |
| return {"findings": items, "severity": [], "recommendations": [], "config_patch": {}} |
| return None |
|
|
|
|
| |
| def run_task(task, run_num, debug=False): |
| try: |
| reset_resp = requests.post(f"{BASE_URL}/reset?task={task}", timeout=30).json() |
| except Exception as e: |
| print(f" {task} run {run_num}: RESET ERROR β {e}") |
| return 0.0 |
|
|
| obs = reset_resp.get("observation", {}) |
| config_text = obs["config"] if isinstance(obs, dict) and "config" in obs else str(obs) |
|
|
| system_prompt = SYSTEM_MAP.get(task, SYSTEM_DEFAULT) |
|
|
| messages = [ |
| {"role": "system", "content": system_prompt}, |
| {"role": "user", "content": f"Audit this AWS config:\n{config_text}"} |
| ] |
|
|
| inputs = tokenizer.apply_chat_template( |
| messages, |
| tokenize=True, |
| add_generation_prompt=True, |
| return_tensors="pt" |
| ).to(model.device) |
|
|
| with torch.no_grad(): |
| outputs = model.generate( |
| input_ids=inputs, |
| attention_mask=torch.ones_like(inputs), |
| max_new_tokens=3072, |
| do_sample=False, |
| use_cache=True, |
| pad_token_id=tokenizer.pad_token_id, |
| eos_token_id=tokenizer.eos_token_id, |
| ) |
|
|
| response_text = tokenizer.decode( |
| outputs[0][inputs.shape[1]:], skip_special_tokens=True |
| ).strip() |
|
|
| parsed = repair_json(response_text) |
| if parsed is None: |
| print(f" {task} run {run_num}: PARSE FAIL β raw: {response_text[:120]}") |
| return 0.0 |
|
|
| if debug: |
| print(f"\n [DEBUG] {task} findings ({len(parsed.get('findings', []))} total):") |
| for i, f in enumerate(parsed.get("findings", [])): |
| print(f" {i+1}. {f}") |
|
|
| try: |
| step = requests.post(f"{BASE_URL}/step", json=parsed, timeout=30).json() |
| if debug: |
| print(f"\n [DEBUG] scorer response: {json.dumps(step, indent=2)[:800]}") |
| except Exception as e: |
| print(f" {task} run {run_num}: STEP ERROR β {e}") |
| return 0.0 |
|
|
| reward = step.get("reward", 0.0) |
| findings = len(parsed.get("findings", [])) |
| print(f" {task} run {run_num}: {reward:.4f} ({findings} findings)") |
| return reward |
|
|
|
|
| tasks = [ |
| "easy_security_group", |
| "medium_s3_policy", |
| "medium_lambda_iam", |
| "hard_rds_cloudtrail", |
| "hard_iam_vpc", |
| ] |
|
|
| DEBUG_TASKS = {"hard_iam_vpc"} |
|
|
| print("=" * 50) |
| all_scores = {} |
| for task in tasks: |
| scores = [run_task(task, i, debug=(task in DEBUG_TASKS and i == 1)) for i in range(1, 4)] |
| avg = sum(scores) / 3 |
| all_scores[task] = avg |
| print(f" >>> {task} AVG: {avg:.4f}\n") |
|
|
| print("=" * 50) |
| print(f"{'FINAL SCORES':^50}") |
| print("=" * 50) |
| for task, score in all_scores.items(): |
| bar = "β" * int(score * 20) |
| print(f"{task:<30} {score:.4f} {bar}") |
|
|