cloud-config-auditor / eval_all.py
kkaustav's picture
Upload eval_all.py with huggingface_hub
8ed0e4d verified
Raw
History Blame Contribute Delete
14.1 kB
import json, re, os, torch, requests, warnings
warnings.filterwarnings("ignore", category=FutureWarning)
warnings.filterwarnings("ignore", message=".*max_new_tokens.*max_length.*")
from huggingface_hub import snapshot_download
from unsloth import FastLanguageModel
from peft import PeftModel
BASE_URL = "https://kkaustav-cloud-config-auditor.hf.space"
BASE_MODEL = "unsloth/Qwen2.5-3B-Instruct-bnb-4bit"
ADAPTER_PATH = "/content/aws-auditor-sft-v5"
if not os.path.exists(ADAPTER_PATH) or not os.path.exists(f"{ADAPTER_PATH}/adapter_config.json"):
print("⬇️ Downloading LoRA adapter...")
snapshot_download(repo_id="kkaustav/aws-security-auditor-lora", local_dir=ADAPTER_PATH)
print("✅ Adapter downloaded")
model, tokenizer = FastLanguageModel.from_pretrained(
model_name=BASE_MODEL,
max_seq_length=4096,
dtype=None,
load_in_4bit=True,
)
model = PeftModel.from_pretrained(model, ADAPTER_PATH)
FastLanguageModel.for_inference(model)
print("✅ Fine-tuned model loaded:", type(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 — MANDATORY, check each separately ===\n"
"12. VPC.FlowLogsEnabled=false — VPC Flow Logs are disabled (HIGH)\n"
"13. VPC.CloudTrailEnabled=false — CloudTrail is disabled (CRITICAL)\n"
"14. VPC.GuardDutyEnabled=false — GuardDuty not enabled (HIGH)\n"
"15. NetworkACL inbound rule: Protocol='-1', CidrBlock='0.0.0.0/0', Action='allow' (HIGH)\n"
"16. NetworkACL outbound rule: Protocol='-1', CidrBlock='0.0.0.0/0', Action='allow' (MEDIUM)\n"
"17. Subnet MapPublicIpOnLaunch=true — public IPs auto-assigned (HIGH)\n\n"
"ABSOLUTE RULES:\n"
"- You MUST produce EXACTLY 17 findings — 11 IAM + 6 VPC.\n"
"- Do NOT stop at finding 11. Continue to findings 12-17.\n"
"- findings array length MUST be 17.\n"
"- severity array length MUST be 17.\n"
"- recommendations array length MUST be 17.\n"
"- Return valid JSON ONLY. No markdown. No code blocks.\n\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 generate_response(config_text, task):
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,
)
return tokenizer.decode(outputs[0][inputs.shape[1]:], skip_special_tokens=True).strip()
def run_task(task, run_num, debug=False):
# Retry reset up to 3 times to handle server fluke
for attempt in range(3):
try:
reset_resp = requests.post(f"{BASE_URL}/reset?task={task}", timeout=30).json()
obs = reset_resp.get("observation", {})
config_text = obs["config"] if isinstance(obs, dict) and "config" in obs else str(obs)
if config_text and len(config_text) > 10:
break
except Exception as e:
if attempt == 2:
print(f" {task} run {run_num}: RESET ERROR — {e}")
return 0.0
response_text = generate_response(config_text, task)
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)
# Retry once if reward is 0 and it's not expected
if reward == 0.0 and task in ["easy_security_group", "medium_s3_policy"]:
print(f" {task} run {run_num}: got 0.0, retrying once...")
response_text = generate_response(config_text, task)
parsed = repair_json(response_text)
if parsed:
try:
step = requests.post(f"{BASE_URL}/step", json=parsed, timeout=30).json()
reward = step.get("reward", 0.0)
except:
pass
print(f" {task} run {run_num}: {reward:.4f} ({len(parsed.get('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}")
overall = sum(all_scores.values()) / len(all_scores)
print(f"\nOVERALL AVERAGE: {overall:.4f}")
repo_path = "/content/cloud-config-auditor"
log_path = os.path.join(repo_path, "run_log_scaled.txt")
with open(log_path, "w") as f:
for task, score in all_scores.items():
f.write(f"{task}: {score:.4f}\n")
f.write(f"OVERALL: {overall:.4f}\n")
print(f"\n✅ run_log_scaled.txt saved to repo!")