SecurityRules.java

package com.cloudforgeci.api.core.rules;

import com.cloudforgeci.api.core.SystemContext;
import com.cloudforgeci.api.core.security.DevSecurityConfiguration;
import com.cloudforgeci.api.core.security.ProductionSecurityConfiguration;
import com.cloudforgeci.api.core.security.StagingSecurityConfiguration;
import com.cloudforgeci.api.core.security.DevSecurityProfileConfiguration;
import com.cloudforgeci.api.core.security.StagingSecurityProfileConfiguration;
import com.cloudforgeci.api.core.security.ProductionSecurityProfileConfiguration;
import com.cloudforgeci.api.interfaces.SecurityConfiguration;
import com.cloudforgeci.api.interfaces.SecurityProfileConfiguration;
import com.cloudforgeci.api.interfaces.Rule;
import com.cloudforge.core.interfaces.FrameworkRules;

import io.github.cdklabs.cdknag.AwsSolutionsChecks;
import io.github.cdklabs.cdknag.HIPAASecurityChecks;
import io.github.cdklabs.cdknag.NagReportFormat;
import io.github.cdklabs.cdknag.PCIDSS321Checks;
import io.github.cdklabs.cdknag.NIST80053R5Checks;
import io.github.cdklabs.cdknag.NagPack;
import software.amazon.awscdk.Aspects;

import com.cloudforge.core.enums.ComplianceMode;
import com.cloudforge.core.enums.SecurityProfile;

import java.util.*;
import java.util.logging.Logger;

/**
 * Security rules installation and compliance framework orchestration.
 *
 * <p>This class coordinates security profile configuration and compliance framework
 * validation. Starting in v3.0.0, it uses auto-discovery to load compliance frameworks,
 * enabling plugin-based extensibility.</p>
 *
 * <h2>Version History:</h2>
 * <ul>
 *   <li><strong>v3.0.0:</strong> Hardcoded framework loading</li>
 *   <li><strong>v3.0.0:</strong> Auto-discovery via {@link FrameworkLoader} with backward compatibility</li>
 *   <li><strong>v4.0.0 (future):</strong> Pure plugin-based architecture</li>
 * </ul>
 *
 * @since 3.0.0
 */
public final class SecurityRules {
  private static final Logger LOG = Logger.getLogger(SecurityRules.class.getName());


  public static void install(SystemContext ctx) {
    // Create and set the SecurityProfileConfiguration in SystemContext
    // Pass deployment context so security profiles can override defaults
    final SecurityProfileConfiguration profileConfig = switch (ctx.security) {
      case DEV        -> new DevSecurityProfileConfiguration(ctx.cfc);
      case STAGING    -> new StagingSecurityProfileConfiguration(ctx.cfc);
      case PRODUCTION -> new ProductionSecurityProfileConfiguration(ctx.cfc);
    };

    ctx.securityProfileConfig.set(profileConfig);

    final SecurityConfiguration p = switch (ctx.security) {
      case DEV        -> new DevSecurityConfiguration();
      case STAGING    -> new StagingSecurityConfiguration();
      case PRODUCTION -> new ProductionSecurityConfiguration();
    };


    ctx.getNode().addValidation(() -> {
      List<String> errs = new ArrayList<>();
      for (Rule r : p.rules(ctx)) errs.addAll(r.check(ctx));
      return errs;
    });

    ctx.once("ProfileWiring:Security:" + p.kind(), () -> {
      p.wire(ctx);
    });

    // Get enabled compliance frameworks (comma-separated list)
    String frameworksConfig = ctx.cfc.complianceFrameworks();

    // Parse enabled frameworks into a set for fast lookup
    Set<String> enabledFrameworks = new LinkedHashSet<>(
        (frameworksConfig == null || frameworksConfig.trim().isEmpty())
            ? Collections.emptySet()
            : Arrays.stream(frameworksConfig.split(","))
                .map(String::trim)
                .map(String::toUpperCase)
                .collect(java.util.stream.Collectors.toSet()));

    // See DeploymentConfig#complianceFrameworksRawOverride's own javadoc -- an on-demand advisory
    // check's escape hatch for tokens with no ComplianceFrameworkType entry at all (e.g.
    // "ISO-27001", or "AWS-BEST-PRACTICES" for cdk-nag's own generic fallback pack below), read
    // directly off the construct tree rather than through DeploymentContext/DeploymentConfig
    // since it deliberately can't round-trip through either. Validated against a fixed charset
    // (rather than trusted verbatim) since it never passes through ComplianceFrameworkType's own
    // enum parsing the way the comma-separated field above does -- every valid token this feature
    // actually offers (see ComplianceService#checkableFrameworks) is already upper-case
    // letters/digits/hyphens, so anything else is dropped rather than logged or installed.
    Object rawOverride = ctx.getNode().tryGetContext("complianceFrameworksRawOverride");
    if (rawOverride instanceof String rawOverrideText && !rawOverrideText.isBlank()) {
        for (String token : rawOverrideText.split(",")) {
            String candidate = token.trim().toUpperCase(java.util.Locale.ROOT);
            if (candidate.matches("[A-Z0-9-]{1,64}")) {
                enabledFrameworks.add(candidate);
            }
        }
    }

    // CDK-nag validation only runs for PRODUCTION with enabled frameworks
    if (ctx.security == SecurityProfile.PRODUCTION && !enabledFrameworks.isEmpty()) {
      applyCdkNagValidation(ctx, enabledFrameworks);
    }

    // CloudForge FrameworkRules validation requires auditManagerEnabled
    if (!Boolean.TRUE.equals(ctx.cfc.auditManagerEnabled())) {
      LOG.info("Skipping CloudForge FrameworkRules validation (auditManagerEnabled = false)");
      return;
    }

    // Log what we're doing
    if (enabledFrameworks.isEmpty()) {
      LOG.info("No compliance frameworks specified - installing only alwaysLoad validators (e.g., ConfigurationValidationRules)");
    } else {
      LOG.info("Installing CloudForge FrameworkRules validation for: " + frameworksConfig);
    }

    // Discover all available compliance frameworks (v3.0.0 plugin architecture)
    List<FrameworkRules<SystemContext>> allFrameworks = FrameworkLoader.discover();

    LOG.info("Discovered " + allFrameworks.size() + " compliance frameworks");

    // Install frameworks in priority order
    int installedCount = 0;
    for (FrameworkRules<SystemContext> framework : allFrameworks) {
      boolean shouldInstall = framework.alwaysLoad() ||
                             enabledFrameworks.contains(framework.frameworkId().toUpperCase());

      if (shouldInstall) {
        try {
          framework.install(ctx);
          installedCount++;
          LOG.info("  ✓ " + framework.displayName() + " (priority=" + framework.priority() + ")");
        } catch (Exception e) {
          LOG.severe("  ✗ Failed to install " + framework.frameworkId() + ": " + e.getMessage());
          throw new RuntimeException("Failed to install compliance framework: " + framework.frameworkId(), e);
        }
      } else {
        LOG.fine("  - Skipping " + framework.frameworkId() + " (not enabled)");
      }
    }

    LOG.info("Successfully installed " + installedCount + " CloudForge FrameworkRules validators");
  }

  /**
   * Auto-apply cdk-nag validation packs based on enabled compliance frameworks.
   *
   * <p>Maps framework names to appropriate cdk-nag rule packs:</p>
   * <ul>
   *   <li><strong>HIPAA:</strong> HIPAASecurityChecks</li>
   *   <li><strong>PCI-DSS:</strong> PCIDSS321Checks</li>
   *   <li><strong>SOC2:</strong> AwsSolutionsChecks</li>
   *   <li><strong>Custom:</strong> AwsSolutionsChecks (fallback)</li>
   * </ul>
   *
   * <p>The enforcement mode is determined by {@code complianceMode}:</p>
   * <ul>
   *   <li><strong>enforce:</strong> Blocks synthesis on violations</li>
   *   <li><strong>advisory:</strong> Logs warnings only</li>
   * </ul>
   *
   * @param ctx the system context containing deployment configuration
   * @param enabledFrameworks set of enabled framework IDs (uppercase)
   * @since 3.1.0
   */
  private static void applyCdkNagValidation(SystemContext ctx, Set<String> enabledFrameworks) {
    if (enabledFrameworks.isEmpty()) {
      LOG.info("No frameworks enabled - skipping cdk-nag validation");
      return;
    }

    boolean enforce = ctx.cfc.complianceMode() == ComplianceMode.ENFORCE;
    LOG.info("Applying cdk-nag validation (mode=" + (enforce ? "enforce" : "advisory") + ")");

    int appliedCount = 0;
    for (String framework : enabledFrameworks) {
      NagPack pack = mapFrameworkToNagPack(framework, enforce);
      if (pack != null) {
        Aspects.of(ctx.getNode().getRoot()).add(pack);
        appliedCount++;
      }
    }

    LOG.info("Applied " + appliedCount + " cdk-nag validation pack(s) across "
        + enabledFrameworks.size() + " enabled framework(s)");
  }

  /**
   * Maps a compliance framework to its corresponding cdk-nag rule pack.
   *
   * @param framework the framework ID (uppercase)
   * @param enforce true to enforce violations, false to log only
   * @return the corresponding NagPack, or null if no mapping exists
   * @since 3.1.0
   */
  private static NagPack mapFrameworkToNagPack(String framework, boolean enforce) {
    // Report formats for compliance auditing. These reports are also how ComplianceMode.ENFORCE
    // blocks: app.synth() does not throw on cdk-nag annotations, so CloudForgeSynthesizer reads
    // <Pack>-<Stack>-NagReport.json after synthesis (see NagReportReader). additionalLoggers is
    // not used because registering a second INagLogger triggers a jsii/cdk-nag StackOverflowError
    // from reentrant kernel calls; the built-in report logger avoids that path.
    var reportFormats = List.of(NagReportFormat.JSON, NagReportFormat.CSV);

    return switch (framework) {
      case "HIPAA" -> HIPAASecurityChecks.Builder.create()
          .logIgnores(!enforce)
          .reports(true)
          .reportFormats(reportFormats)
          .build();
      case "PCI-DSS" -> PCIDSS321Checks.Builder.create()
          .logIgnores(!enforce)
          .reports(true)
          .reportFormats(reportFormats)
          .build();
      case "SOC2" -> AwsSolutionsChecks.Builder.create()
          .logIgnores(!enforce)
          .reports(true)
          .reportFormats(reportFormats)
          .build();
      // FEDRAMP/FEDRAMPHIGH: NIST 800-53 Rev 5 is the control baseline both share -- cdk-nag has
      // no separate Low/Moderate/High pack, so one mapping covers both tokens. Runs alongside
      // FedRampRules.java rather than replacing it, same as HIPAA/PCI-DSS/SOC2 pairing a cdk-nag
      // pack with their own FrameworkRules validator.
      case "FEDRAMP", "FEDRAMP-HIGH", "FEDRAMPHIGH" -> NIST80053R5Checks.Builder.create()
          .logIgnores(!enforce)
          .reports(true)
          .reportFormats(reportFormats)
          .build();
      // Custom frameworks: fallback to AWS Solutions best practices
      default -> {
        LOG.info("  - Applying AwsSolutionsChecks (fallback) for an unrecognized/custom framework");
        yield AwsSolutionsChecks.Builder.create()
            .logIgnores(!enforce)
            .reports(true)
            .reportFormats(reportFormats)
            .build();
      }
    };
  }
}