// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.
// NERF is a trademark of defconxt.

/**
 * NERF GitHub Actions Integration — PR-Level Security Review.
 *
 * Analyzes PR diffs for security-relevant changes, detects hardcoded secrets,
 * identifies new attack surface, and posts structured findings as PR comments.
 * Supports SARIF output for GitHub Code Scanning and generates reusable
 * workflow/action YAML for GitHub Marketplace distribution.
 */

import { createHash, randomUUID } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { SarifReport, SarifRule, SarifResult } from './sarif.js';

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function _getVersion() {
  try {
    const pkg = JSON.parse(readFileSync(join(import.meta.dirname || '.', '..', '..', 'package.json'), 'utf8'));
    return pkg.version || '0.0.0-dev';
  } catch {
    return '0.0.0-dev';
  }
}

// ---------------------------------------------------------------------------
// Enums
// ---------------------------------------------------------------------------

/** @type {Readonly<{CRITICAL: string, HIGH: string, MEDIUM: string, LOW: string, INFO: string}>} */
const RiskLevel = Object.freeze({
  CRITICAL: 'critical',
  HIGH: 'high',
  MEDIUM: 'medium',
  LOW: 'low',
  INFO: 'info',
});

/** @type {Readonly<{API_KEY: string, PASSWORD: string, TOKEN: string, PRIVATE_KEY: string, CONNECTION_STRING: string, AWS_CREDENTIAL: string, GENERIC: string}>} */
const SecretType = Object.freeze({
  API_KEY: 'api_key',
  PASSWORD: 'password',
  TOKEN: 'token',
  PRIVATE_KEY: 'private_key',
  CONNECTION_STRING: 'connection_string',
  AWS_CREDENTIAL: 'aws_credential',
  GENERIC: 'generic',
});

// ---------------------------------------------------------------------------
// Data classes
// ---------------------------------------------------------------------------

class SecretFinding {
  /**
   * @param {object} opts
   * @param {string} opts.secretType - one of SecretType values
   * @param {string} opts.filePath
   * @param {number} opts.lineNumber
   * @param {string} opts.matchedPattern
   * @param {string} opts.snippet
   * @param {number} opts.confidence
   * @param {string} [opts.recommendation]
   */
  constructor(opts = {}) {
    this.secretType = opts.secretType || SecretType.GENERIC;
    this.filePath = opts.filePath || '';
    this.lineNumber = opts.lineNumber || 0;
    this.matchedPattern = opts.matchedPattern || '';
    this.snippet = opts.snippet || '';
    this.confidence = opts.confidence || 0;
    this.recommendation =
      opts.recommendation || 'Rotate immediately and move to a secrets manager.';
  }

  get fingerprint() {
    const raw = `${this.secretType}:${this.filePath}:${this.matchedPattern}`;
    return createHash('sha256').update(raw).digest('hex').slice(0, 16);
  }
}

class DiffAnalysis {
  constructor(opts = {}) {
    this.filesChanged = opts.filesChanged || [];
    this.endpointsAdded = opts.endpointsAdded || [];
    this.endpointsModified = opts.endpointsModified || [];
    this.secrets = opts.secrets || [];
    this.authChanges = opts.authChanges || [];
    this.cryptoChanges = opts.cryptoChanges || [];
    this.sqlChanges = opts.sqlChanges || [];
    this.inputChanges = opts.inputChanges || [];
    this.fileChanges = opts.fileChanges || [];
    this.networkChanges = opts.networkChanges || [];
    this.dependencyChanges = opts.dependencyChanges || [];
    this.riskLevel = opts.riskLevel || RiskLevel.INFO;
    this.summary = opts.summary || '';
  }

  get hasSecurityFindings() {
    return !!(
      this.secrets.length ||
      this.authChanges.length ||
      this.cryptoChanges.length ||
      this.sqlChanges.length
    );
  }
}

class PRReviewResult {
  constructor(opts = {}) {
    this.repo = opts.repo || '';
    this.prNumber = opts.prNumber || 0;
    this.diffAnalysis = opts.diffAnalysis || new DiffAnalysis();
    this.scanFindings = opts.scanFindings || [];
    this.regressions = opts.regressions || [];
    this.reviewedAt = opts.reviewedAt || new Date().toISOString();
    this.reviewId = opts.reviewId || randomUUID().replace(/-/g, '').slice(0, 12);
  }

  get riskLevel() {
    return this.diffAnalysis.riskLevel;
  }

  get totalFindings() {
    return (
      this.diffAnalysis.secrets.length +
      this.scanFindings.length +
      this.regressions.length
    );
  }
}

// ---------------------------------------------------------------------------
// Regex patterns — compiled once at module load
// ---------------------------------------------------------------------------

const _AUTH_RE = [
  /\b(authenticate|authorize|login|logout|session|jwt|oauth)\b/i,
  /\b(password|passwd|credential|api_key|secret)\b/i,
  /@(require_auth|login_required|permission_required)/i,
];

const _CRYPTO_RE = [
  /\b(md5|sha1|sha256|sha512|hmac|bcrypt|scrypt|argon2)\b/i,
  /\b(encrypt|decrypt|cipher|aes|rsa|ecdsa|sign|verify)\b/i,
  /\b(ssl|tls|certificate|x509|pem|pkcs)\b/i,
];

const _SQL_RE = [
  /\b(execute|raw_sql|text\(|cursor\.execute)\b/i,
  /(SELECT|INSERT|UPDATE|DELETE|DROP|ALTER)\s+/i,
  /f['"].*\{.*\}.*(?:SELECT|INSERT|UPDATE|DELETE)/i,
];

const _INPUT_RE = [
  /\b(request\.(get|post|json|form|args|params))\b/i,
  /\b(innerHTML|dangerouslySetInnerHTML|eval|exec)\b/i,
  /\b(unserialize|pickle\.loads|yaml\.load)\b/i,
];

const _FILE_RE = [
  /\b(subprocess|os\.system|popen|exec|spawn)\b/i,
  /\b(open|read|write|unlink|rmtree|chmod|chown)\b/i,
];

const _NETWORK_RE = [
  /\b(fetch|axios|request|http\.get|https\.get)\b/i,
  /\b(WebSocket|XMLHttpRequest|cors)\b/i,
];

const _DEPENDENCY_RE = [
  /\b(require|import)\b/i,
  /\b(dependencies|devDependencies|peerDependencies)\b/i,
];

const _ENDPOINT_RE = [
  /@(?:app|router|blueprint)\.(get|post|put|patch|delete)\(['"]([^'"]+)/i,
  /path\(['"]([^'"]+)['"]/i,
  /router\.(get|post|put|patch|delete)\(['"]([^'"]+)/i,
  /@(Get|Post|Put|Patch|Delete)Mapping\(['"]([^'"]+)/i,
];

/** @type {Array<[RegExp, string, number]>} */
const _SECRET_RE = [
  [/AKIA[0-9A-Z]{16}/, SecretType.AWS_CREDENTIAL, 0.95],
  [/-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/, SecretType.PRIVATE_KEY, 0.99],
  [/ghp_[A-Za-z0-9_]{36}/, SecretType.TOKEN, 0.95],
  [/sk-[A-Za-z0-9]{48}/, SecretType.API_KEY, 0.90],
  [/xox[bpas]-[A-Za-z0-9\-]{10,}/, SecretType.TOKEN, 0.90],
  [/(?:api[_-]?key|apikey)\s*[:=]\s*['"]([A-Za-z0-9_\-]{20,})['"]/i, SecretType.API_KEY, 0.75],
  [/(?:password|passwd|pwd)\s*[:=]\s*['"]([^'"]{8,})['"]/i, SecretType.PASSWORD, 0.70],
  [/(?:secret|token)\s*[:=]\s*['"]([A-Za-z0-9_\-]{16,})['"]/i, SecretType.TOKEN, 0.70],
  [/(?:postgres|mysql|mongodb):\/\/[^\s'"]{10,}/i, SecretType.CONNECTION_STRING, 0.85],
];

// ---------------------------------------------------------------------------
// Severity emoji map
// ---------------------------------------------------------------------------

const SEV_EMOJI = Object.freeze({
  critical: '🔴',
  high: '🟠',
  medium: '🟡',
  low: '🔵',
  info: '⚪',
});

// ---------------------------------------------------------------------------
// SecurityDiffAnalyzer
// ---------------------------------------------------------------------------

class SecurityDiffAnalyzer {
  /**
   * Parse a unified diff and identify security-relevant changes.
   * @param {string} diffText
   * @returns {DiffAnalysis}
   */
  analyzeDiff(diffText) {
    const analysis = new DiffAnalysis({
      filesChanged: this._extractFiles(diffText),
      endpointsAdded: this.extractEndpointsFromDiff(diffText),
      secrets: this.detectSecretsInDiff(diffText),
    });

    for (const line of this._addedLines(diffText)) {
      this._classifyLine(line, analysis);
    }

    analysis.riskLevel = this._riskFrom(analysis);
    analysis.summary = this._summarize(analysis);
    return analysis;
  }

  /**
   * Extract new/modified API endpoint paths from added diff lines.
   * @param {string} diffText
   * @returns {string[]}
   */
  extractEndpointsFromDiff(diffText) {
    const endpoints = [];
    for (const line of this._addedLines(diffText)) {
      for (const pat of _ENDPOINT_RE) {
        const m = pat.exec(line);
        if (m) {
          const path = m[m.length > 2 ? 2 : 1];
          if (path && !endpoints.includes(path)) {
            endpoints.push(path);
          }
        }
      }
    }
    return endpoints;
  }

  /**
   * Scan added lines for hardcoded secrets and credentials.
   * @param {string} diffText
   * @returns {SecretFinding[]}
   */
  detectSecretsInDiff(diffText) {
    const findings = [];
    let curFile = '<unknown>';
    let lnum = 0;

    for (const raw of diffText.split('\n')) {
      if (raw.startsWith('+++ b/')) {
        curFile = raw.slice(6);
      } else if (raw.startsWith('@@ ')) {
        const m = raw.match(/\+(\d+)/);
        lnum = m ? parseInt(m[1], 10) : 0;
      } else if (raw.startsWith('+') && !raw.startsWith('+++')) {
        lnum += 1;
        const content = raw.slice(1);
        for (const [pattern, stype, conf] of _SECRET_RE) {
          if (pattern.test(content)) {
            findings.push(
              new SecretFinding({
                secretType: stype,
                filePath: curFile,
                lineNumber: lnum,
                matchedPattern: pattern.source.slice(0, 60),
                snippet: content.trim().slice(0, 80),
                confidence: conf,
              }),
            );
            break;
          }
        }
      } else if (!raw.startsWith('-')) {
        lnum += 1;
      }
    }
    return findings;
  }

  /**
   * Classify risk from raw diff text.
   * @param {string} diffText
   * @returns {string}
   */
  classifyRisk(diffText) {
    return this.analyzeDiff(diffText).riskLevel;
  }

  /**
   * Extract changed file paths from diff.
   * @param {string} diff
   * @returns {string[]}
   */
  _extractFiles(diff) {
    return diff
      .split('\n')
      .filter((ln) => ln.startsWith('+++ b/'))
      .map((ln) => ln.slice(6));
  }

  /**
   * Extract added lines (lines starting with '+' excluding file headers).
   * @param {string} diff
   * @returns {string[]}
   */
  _addedLines(diff) {
    return diff
      .split('\n')
      .filter((ln) => ln.startsWith('+') && !ln.startsWith('+++'))
      .map((ln) => ln.slice(1));
  }

  /**
   * Classify a single diff line into analysis buckets.
   * @param {string} line
   * @param {DiffAnalysis} a
   */
  _classifyLine(line, a) {
    const stripped = line.trim();
    const buckets = [
      [_AUTH_RE, a.authChanges],
      [_CRYPTO_RE, a.cryptoChanges],
      [_SQL_RE, a.sqlChanges],
      [_INPUT_RE, a.inputChanges],
      [_FILE_RE, a.fileChanges],
      [_NETWORK_RE, a.networkChanges],
      [_DEPENDENCY_RE, a.dependencyChanges],
    ];
    for (const [pats, bucket] of buckets) {
      if (pats.some((p) => p.test(line))) {
        bucket.push(stripped);
      }
    }
  }

  /**
   * Determine risk level from analysis.
   * @param {DiffAnalysis} a
   * @returns {string}
   */
  _riskFrom(a) {
    if (a.secrets.length || a.authChanges.length || a.cryptoChanges.length) {
      return RiskLevel.HIGH;
    }
    if (a.sqlChanges.length || a.inputChanges.length) {
      return RiskLevel.MEDIUM;
    }
    if (a.fileChanges.length || a.endpointsAdded.length) {
      return RiskLevel.LOW;
    }
    return RiskLevel.INFO;
  }

  /**
   * Produce human-readable summary.
   * @param {DiffAnalysis} a
   * @returns {string}
   */
  _summarize(a) {
    const parts = [`${a.filesChanged.length} file(s) changed`];
    const checks = [
      ['secret(s)', a.secrets],
      ['auth change(s)', a.authChanges],
      ['crypto change(s)', a.cryptoChanges],
      ['SQL change(s)', a.sqlChanges],
      ['new endpoint(s)', a.endpointsAdded],
    ];
    for (const [label, items] of checks) {
      if (items.length) {
        parts.push(`${items.length} ${label}`);
      }
    }
    return parts.join('; ');
  }
}

// ---------------------------------------------------------------------------
// FindingFormatter
// ---------------------------------------------------------------------------

class FindingFormatter {
  /**
   * Format a full PR review result as a GitHub markdown comment.
   * @param {PRReviewResult} findings
   * @returns {string}
   */
  toPrComment(findings) {
    const da = findings.diffAnalysis;
    const risk = findings.riskLevel;
    const lines = [
      `## ${SEV_EMOJI[risk] || '⚪'} NERF Security Review — ${risk.toUpperCase()} risk`,
      '',
      `> Review \`${findings.reviewId}\` | ${findings.reviewedAt}`,
      '',
      `**Summary:** ${da.summary}`,
      '',
    ];

    if (da.secrets.length) {
      lines.push(
        '### 🔑 Hardcoded Secrets',
        '',
        '| # | Type | File | Line | Confidence |',
        '|---|------|------|------|------------|',
      );
      da.secrets.forEach((s, i) => {
        lines.push(
          `| ${i + 1} | \`${s.secretType}\` | \`${s.filePath}\` | ${s.lineNumber} | ${Math.round(s.confidence * 100)}% |`,
        );
      });
      lines.push('');
    }

    const sections = [
      ['🔐 Auth Changes', da.authChanges],
      ['🔒 Crypto Changes', da.cryptoChanges],
      ['🗄️ SQL Changes', da.sqlChanges],
      ['📥 Input Handling', da.inputChanges],
      ['📂 File/Process Ops', da.fileChanges],
    ];
    for (const [label, items] of sections) {
      if (items.length) {
        lines.push(`### ${label}`, '');
        for (const it of items.slice(0, 10)) {
          lines.push(`- \`${it.slice(0, 120)}\``);
        }
        if (items.length > 10) {
          lines.push(`- _… and ${items.length - 10} more_`);
        }
        lines.push('');
      }
    }

    if (da.endpointsAdded.length) {
      lines.push('### 🌐 New Endpoints', '');
      for (const ep of da.endpointsAdded) {
        lines.push(`- \`${ep}\``);
      }
      lines.push('');
    }

    if (findings.regressions.length) {
      lines.push('### ⚠️ Regressions', '');
      for (const r of findings.regressions) {
        lines.push(`- **${r.title || 'Untitled'}** — ${r.description || ''}`);
      }
      lines.push('');
    }

    lines.push('---', '_Generated by [NERF](https://github.com/defconxt/nerf)_');
    return lines.join('\n');
  }

  /**
   * Produce SARIF v2.1.0 for GitHub Code Scanning.
   * @param {PRReviewResult} findings
   * @returns {object}
   */
  toSarif(findings) {
    const results = [];
    const rules = [];
    const seen = new Set();

    for (const s of findings.diffAnalysis.secrets) {
      const rid = `nerf/secret-${s.secretType}`;
      if (!seen.has(rid)) {
        seen.add(rid);
        rules.push({
          id: rid,
          name: `HardcodedSecret_${s.secretType}`,
          shortDescription: { text: `Hardcoded ${s.secretType}` },
          defaultConfiguration: { level: 'error' },
        });
      }
      results.push({
        ruleId: rid,
        message: { text: `Hardcoded ${s.secretType} in diff` },
        locations: [
          {
            physicalLocation: {
              artifactLocation: { uri: s.filePath },
              region: { startLine: s.lineNumber },
            },
          },
        ],
        fingerprints: { 'nerf/v1': s.fingerprint },
      });
    }

    return {
      $schema:
        'https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-schema-2.1.0.json',
      version: '2.1.0',
      runs: [
        {
          tool: {
            driver: {
              name: 'NERF',
              version: _getVersion(),
              informationUri: 'https://github.com/defconxt/nerf',
              rules,
            },
          },
          results,
        },
      ],
    };
  }

  /**
   * Serialize review result as structured JSON.
   * @param {PRReviewResult} findings
   * @returns {string}
   */
  toJson(findings) {
    const da = findings.diffAnalysis;
    return JSON.stringify(
      {
        review_id: findings.reviewId,
        repo: findings.repo,
        pr_number: findings.prNumber,
        risk_level: findings.riskLevel,
        reviewed_at: findings.reviewedAt,
        total_findings: findings.totalFindings,
        diff_analysis: {
          files_changed: da.filesChanged,
          endpoints_added: da.endpointsAdded,
          secrets_count: da.secrets.length,
          auth_changes: da.authChanges.length,
          crypto_changes: da.cryptoChanges.length,
          sql_changes: da.sqlChanges.length,
        },
        secrets: da.secrets.map((s) => ({
          type: s.secretType,
          file: s.filePath,
          line: s.lineNumber,
          confidence: s.confidence,
          fingerprint: s.fingerprint,
        })),
        regressions: findings.regressions,
      },
      null,
      2,
    );
  }
}

// ---------------------------------------------------------------------------
// PRSecurityReview — orchestrates PR-level reviews via GitHub API
// ---------------------------------------------------------------------------

class PRSecurityReview {
  /**
   * @param {object} [opts]
   * @param {string} [opts.githubToken]
   * @param {string} [opts.baseUrl]
   */
  constructor(opts = {}) {
    this.githubToken = opts.githubToken || '';
    this._api = opts.baseUrl || 'https://api.github.com';
    this._analyzer = new SecurityDiffAnalyzer();
    this._formatter = new FindingFormatter();
  }

  /**
   * Fetch PR diff, analyse, and return review result.
   * @param {string} repo
   * @param {number} prNumber
   * @returns {Promise<PRReviewResult>}
   */
  async reviewPr(repo, prNumber) {
    const diffText = await this._fetchDiff(repo, prNumber);
    const analysis = this._analyzer.analyzeDiff(diffText);
    return new PRReviewResult({
      repo,
      prNumber,
      diffAnalysis: analysis,
    });
  }

  /**
   * Post findings as a PR comment.
   * @param {string} repo
   * @param {number} prNumber
   * @param {PRReviewResult} findings
   * @returns {Promise<boolean>}
   */
  async postComment(repo, prNumber, findings) {
    const body = this._formatter.toPrComment(findings);
    const url = `${this._api}/repos/${repo}/issues/${prNumber}/comments`;
    return this._post(url, { body });
  }

  /**
   * @param {string} repo
   * @param {number} prNumber
   * @returns {Promise<string>}
   */
  async _fetchDiff(repo, prNumber) {
    const url = `${this._api}/repos/${repo}/pulls/${prNumber}`;
    const headers = { Accept: 'application/vnd.github.diff' };
    if (this.githubToken) {
      headers.Authorization = `Bearer ${this.githubToken}`;
    }
    const resp = await fetch(url, { headers, signal: AbortSignal.timeout(30000) });
    return resp.text();
  }

  /**
   * @param {string} url
   * @param {object} payload
   * @returns {Promise<boolean>}
   */
  async _post(url, payload) {
    const headers = {
      Accept: 'application/vnd.github+json',
      'Content-Type': 'application/json',
    };
    if (this.githubToken) {
      headers.Authorization = `Bearer ${this.githubToken}`;
    }
    try {
      const resp = await fetch(url, {
        method: 'POST',
        headers,
        body: JSON.stringify(payload),
        signal: AbortSignal.timeout(30000),
      });
      return resp.status >= 200 && resp.status < 300;
    } catch {
      return false;
    }
  }
}

// ---------------------------------------------------------------------------
// WorkflowGenerator
// ---------------------------------------------------------------------------

const _WORKFLOW_TPL = `# Generated by NERF — do not edit manually.
name: NERF Security Review
on:
  pull_request:
    types: [opened, synchronize, reopened]
  workflow_dispatch:
    inputs:
      scan_profile:
        description: "Scan profile"
        default: "{{profile}}"
        type: choice
        options: [pentest, recon, architecture]
permissions: { contents: read, pull-requests: write, security-events: write }
jobs:
  security-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: actions/setup-node@v5
        with: { node-version: "22" }
      - run: npm install -g @defconxt/nerf
      - name: Run PR Security Review
        env:
          GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
          NERF_PROFILE: \${{ inputs.scan_profile || '{{profile}}' }}
        run: >-
          nerf review-pr
          --repo "\${{ github.repository }}"
          --pr "\${{ github.event.pull_request.number }}"
          --profile "$NERF_PROFILE"
          --output sarif --output-file results.sarif
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with: { sarif_file: results.sarif }
      - name: Post PR Comment
        if: github.event_name == 'pull_request'
        env:
          GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
        run: >-
          nerf review-pr
          --repo "\${{ github.repository }}"
          --pr "\${{ github.event.pull_request.number }}"
          --comment
`;

const _ACTION_TPL = `# Generated by NERF — do not edit manually.
name: "NERF Security Review"
description: "Automated PR-level security review. Detects secrets, auth regressions, new attack surface."
author: "defconxt"
branding: { icon: shield, color: purple }
inputs:
  scan_profile: { description: "Scan profile", required: false, default: "pentest" }
  github_token: { description: "GitHub token", required: true }
  post_comment: { description: "Post findings as PR comment", required: false, default: "true" }
  sarif_output: { description: "SARIF output path", required: false, default: "nerf-results.sarif" }
outputs:
  risk_level: { description: "Overall risk level (high|medium|low|info)" }
  total_findings: { description: "Total number of findings" }
  review_id: { description: "Unique review identifier" }
runs:
  using: "composite"
  steps:
    - uses: actions/setup-node@v5
      with: { node-version: "22" }
    - { shell: bash, run: "npm install -g @defconxt/nerf" }
    - name: Run Security Review
      shell: bash
      env:
        GITHUB_TOKEN: \${{ inputs.github_token }}
        NERF_PROFILE: \${{ inputs.scan_profile }}
      run: >-
        nerf review-pr
        --repo "\${{ github.repository }}"
        --pr "\${{ github.event.pull_request.number }}"
        --profile "$NERF_PROFILE"
        --output sarif --output-file "\${{ inputs.sarif_output }}"
    - name: Post Comment
      if: inputs.post_comment == 'true'
      shell: bash
      env: { GITHUB_TOKEN: "\${{ inputs.github_token }}" }
      run: >-
        nerf review-pr
        --repo "\${{ github.repository }}"
        --pr "\${{ github.event.pull_request.number }}"
        --comment
`;

class WorkflowGenerator {
  /**
   * Generate `.github/workflows/nerf-pi.yml`.
   * @param {string} [scanProfile='pentest']
   * @returns {string}
   */
  generateWorkflow(scanProfile = 'pentest') {
    return _WORKFLOW_TPL.replace(/\{\{profile\}\}/g, scanProfile);
  }

  /**
   * Generate `action.yml` for GitHub Marketplace.
   * @returns {string}
   */
  generateActionYaml() {
    return _ACTION_TPL;
  }
}

// ---------------------------------------------------------------------------
// Exports
// ---------------------------------------------------------------------------

export {
  // Helpers
  _getVersion,
  // Enums
  RiskLevel,
  SecretType,
  SEV_EMOJI,
  // Data classes
  SecretFinding,
  DiffAnalysis,
  PRReviewResult,
  // Analyzers
  SecurityDiffAnalyzer,
  // Formatters
  FindingFormatter,
  // PR Review
  PRSecurityReview,
  // Generators
  WorkflowGenerator,
};
