Files
A-Team-Security-Infra-Agent…/backend/mcp_cis_server.py
nogueiraguh a5b8f6903a feat: resource type validation, 100K max tokens, and auto-split improvements
- Add SQLite-based resource type validation with suggestions for invalid types
- Increase Terraform max_tokens from 32K to 100K for large infra generation
- Generate resource reference at startup to survive volume mount overwrites
- Add invalid resource type examples to system prompt (RPC peer, subnet route table)
- Add prompt size logging for Terraform agent
- Translate remaining Portuguese code comment to English
2026-03-09 01:10:33 -03:00

698 lines
45 KiB
Python

#!/usr/bin/env python3
"""
MCP Server: CIS OCI Foundations Benchmark 3.0 Compliance Scanner
Granular per-section data collection — each section only fetches the OCI data
it needs, avoiding the slow full-tenancy scan.
"""
import asyncio
import concurrent.futures
import datetime
import json
import os
import time
import traceback
from configparser import ConfigParser
from functools import partial
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
# ──────────────────────────────────────────────────────────────────────
server = Server("cis-compliance")
# session_key -> {checker, sections_collected: set, sections_analyzed: set, ...}
_sessions: dict = {}
SESSION_TTL = 7200 # 2 hours — reduce cold starts
TOOL_TIMEOUT = 1800 # 30 min max per tool call
TOOL_RETRIES = 1 # 1 automatic retry on failure
def _session_key(config_id: str, regions: list[str] | None = None) -> str:
"""Build cache key that includes region filter so different scopes are cached separately."""
if not regions:
return config_id
return f"{config_id}:{','.join(sorted(regions))}"
OCI_CONFIGS_DIR = os.environ.get("OCI_CONFIGS_DIR", "/data/oci_configs")
# ──────────────────────────────────────────────────────────────────────
# Per-section data collectors: which private methods to call
# ──────────────────────────────────────────────────────────────────────
# Section -> (home_region_collectors, regional_collectors, analysis_methods)
SECTION_DEPS = {
"iam": {
"home": [
"_CIS_Report__identity_read_users",
"_CIS_Report__identity_read_tenancy_password_policy",
"_CIS_Report__identity_read_dynamic_groups",
"_CIS_Report__identity_read_tenancy_policies",
],
"regional": [],
"analyze": [
"_CIS_Report__cis_check_iam_policies",
"_CIS_Report__cis_check_password_policies",
"_CIS_Report__cis_check_users",
"_CIS_Report__cis_check_dynamic_groups",
],
},
"networking": {
"home": [],
"regional": [
"_CIS_Report__network_read_network_security_lists",
"_CIS_Report__network_read_network_security_groups_rules",
"_CIS_Report__network_read_network_vcns",
"_CIS_Report__network_read_network_subnets",
"_CIS_Report__adb_read_adbs",
"_CIS_Report__oic_read_oics",
"_CIS_Report__oac_read_oacs",
],
"analyze": ["_CIS_Report__cis_check_network_security"],
},
"compute": {
"home": [],
"regional": ["_CIS_Report__core_instance_read_compute"],
"analyze": ["_CIS_Report__cis_check_compute_instances"],
},
"logging_monitoring": {
"home": [
"_CIS_Report__identity_read_tag_defaults",
"_CIS_Report__identity_read_tenancy_policies",
],
"regional": [
"_CIS_Report__events_read_event_rules",
"_CIS_Report__ons_read_subscriptions",
"_CIS_Report__logging_read_log_groups_and_logs",
"_CIS_Report__kms_read_keys",
"_CIS_Report__os_read_buckets",
"_CIS_Report__network_read_network_subnets",
"_CIS_Report__network_read_network_capturefilters",
],
"analyze": ["_CIS_Report__cis_check_tagging_and_monitoring"],
},
"storage": {
"home": [],
"regional": [
"_CIS_Report__os_read_buckets",
"_CIS_Report__block_volume_read_block_volumes",
"_CIS_Report__boot_volume_read_boot_volumes",
"_CIS_Report__fss_read_fsss",
],
"analyze": ["_CIS_Report__cis_check_storage"],
},
"asset_management": {
"home": [],
"regional": ["_CIS_Report__search_resources_in_root_compartment"],
"analyze": ["_CIS_Report__cis_check_assets"],
},
}
SECTION_NAMES = {
"iam": "Identity and Access Management",
"networking": "Networking",
"compute": "Compute",
"logging_monitoring": "Logging and Monitoring",
"storage": "Storage",
"asset_management": "Asset Management",
}
SECTION_MAP = {"1": "iam", "2": "networking", "3": "compute", "4": "logging_monitoring", "5": "storage", "6": "asset_management"}
# ──────────────────────────────────────────────────────────────────────
# Helpers
# ──────────────────────────────────────────────────────────────────────
def _serialize_oci_obj(obj, max_depth=3):
if max_depth <= 0: return str(obj)
if obj is None: return None
if isinstance(obj, (str, int, float, bool)): return obj
if isinstance(obj, datetime.datetime): return obj.isoformat()
if isinstance(obj, (list, tuple)): return [_serialize_oci_obj(i, max_depth-1) for i in obj]
if isinstance(obj, dict): return {k: _serialize_oci_obj(v, max_depth-1) for k, v in obj.items()}
if hasattr(obj, "swagger_types"):
return {a: _serialize_oci_obj(getattr(obj, a, None), max_depth-1) for a in obj.swagger_types}
return str(obj)
def _get_config_path(config_id: str) -> str:
return os.path.join(OCI_CONFIGS_DIR, config_id, "config")
def _list_available_configs() -> list[dict]:
configs = []
if not os.path.isdir(OCI_CONFIGS_DIR): return configs
for entry in sorted(os.listdir(OCI_CONFIGS_DIR)):
config_path = os.path.join(OCI_CONFIGS_DIR, entry, "config")
if os.path.isfile(config_path):
info = {"config_id": entry, "config_path": config_path}
try:
cp = ConfigParser(); cp.read(config_path)
sec = cp.sections()[0] if cp.sections() else "DEFAULT"
info["tenancy_ocid"] = cp.get(sec, "tenancy", fallback="N/A")
info["region"] = cp.get(sec, "region", fallback="N/A")
info["user_ocid"] = cp.get(sec, "user", fallback="N/A")
except Exception: pass
configs.append(info)
return configs
def _get_or_create_checker(config_id: str, regions: list[str] | None = None):
"""Return cached session or create a new CIS_Report instance (fast — no data collection)."""
key = _session_key(config_id, regions)
now = time.time()
if key in _sessions:
s = _sessions[key]
if now - s["created_at"] < SESSION_TTL:
return s
del _sessions[key]
import cis_reports
config_path = _get_config_path(config_id)
if not os.path.isfile(config_path):
raise FileNotFoundError(f"OCI config not found: {config_path}")
cp = ConfigParser(); cp.read(config_path)
profile = cp.sections()[0] if cp.sections() else "DEFAULT"
config, signer = cis_reports.create_signer(
file_location=config_path, config_profile=profile,
is_instance_principals=False, is_delegation_token=False, is_security_token=False,
)
regions_str = ",".join(regions) if regions else None
checker = cis_reports.CIS_Report(
config=config, signer=signer, proxy=None, output_bucket=None,
report_directory="/tmp/cis_reports", report_prefix="",
report_summary_json=False, print_to_screen="FALSE",
regions_to_run_in=regions_str, raw_data=False, obp=False,
redact_output=False, debug=False, all_resources=False,
)
session = {
"checker": checker, "created_at": now, "config_id": config_id,
"regions": regions, "session_key": key,
"base_collected": False, "sections_collected": set(), "sections_analyzed": set(),
}
_sessions[key] = session
return session
def _ensure_base(session):
"""Collect compartments, groups, domains — shared prereqs for all sections."""
if session["base_collected"]:
return
checker = session["checker"]
base_methods = [
"_CIS_Report__identity_read_compartments",
"_CIS_Report__cloud_guard_read_cloud_guard_configuration",
"_CIS_Report__identity_read_domains",
"_CIS_Report__identity_read_groups",
"_CIS_Report__identity_read_availability_domains",
]
def _run_base(fn_name):
try:
getattr(checker, fn_name)()
except Exception as e:
print(f"Warning: {fn_name} failed: {e}")
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as ex:
list(ex.map(_run_base, base_methods))
session["base_collected"] = True
def _collect_section(session, section: str):
"""Collect data for a specific section only."""
if section in session["sections_collected"]:
return
_ensure_base(session)
checker = session["checker"]
deps = SECTION_DEPS.get(section)
if not deps:
return
if "collection_errors" not in session:
session["collection_errors"] = {}
errors = []
def _run_method(fn_name):
try:
getattr(checker, fn_name)()
except Exception as e:
errors.append({"collector": fn_name, "error": str(e)[:200]})
print(f"Warning: {fn_name} failed: {e}")
# Home region collectors (parallel)
if deps["home"]:
with concurrent.futures.ThreadPoolExecutor(max_workers=len(deps["home"])) as ex:
list(ex.map(_run_method, deps["home"]))
# Regional collectors (parallel with thread pool)
if deps["regional"]:
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as ex:
list(ex.map(_run_method, deps["regional"]))
if errors:
session["collection_errors"][section] = errors
session["sections_collected"].add(section)
def _analyze_section(session, section: str):
"""Run CIS analysis for a specific section."""
if section in session["sections_analyzed"]:
return
_collect_section(session, section)
checker = session["checker"]
deps = SECTION_DEPS.get(section)
if not deps:
return
# Initialize regional findings data if not set
if not hasattr(checker, '_CIS_Report__cis_regional_findings_data') or not checker._CIS_Report__cis_regional_findings_data:
checker._CIS_Report__cis_regional_findings_data = {}
cis_regional_checks = checker._CIS_Report__cis_regional_checks
for check in cis_regional_checks:
checker._CIS_Report__cis_regional_findings_data[check] = {}
for rk in checker._CIS_Report__regions:
checker._CIS_Report__cis_regional_findings_data[check][rk] = None
for method_name in deps["analyze"]:
try:
getattr(checker, method_name)()
except Exception as e:
print(f"Warning: {method_name} failed: {e}")
session["sections_analyzed"].add(section)
def _check_id_to_section(check_id: str) -> str:
prefix = check_id.split(".")[0]
return SECTION_MAP.get(prefix, "unknown")
def _get_benchmark(checker):
return checker.cis_foundations_benchmark_3_0
def _section_summary(checker, section_key: str) -> dict:
benchmark = _get_benchmark(checker)
checks = []
passed = failed = 0
for rec_id, check in benchmark.items():
if _check_id_to_section(rec_id) != section_key:
continue
status = check.get("Status")
if status is True: passed += 1
elif status is False: failed += 1
checks.append({
"check_id": rec_id, "id": check["id"], "title": check["Title"],
"status": "PASS" if status is True else ("FAIL" if status is False else "N/A"),
"level": check.get("Level", "N/A"),
"findings_count": len(check.get("Findings", [])),
"findings": _serialize_oci_obj(check.get("Findings", [])[:20]),
})
total = passed + failed
score = round(passed / total * 100, 1) if total > 0 else 0
return {"section": SECTION_NAMES.get(section_key, section_key), "compliance_score": score,
"passed": passed, "failed": failed, "checks": checks}
def _finding_in_compartment(finding, compartment_id):
if isinstance(finding, dict):
fcomp = finding.get("compartment_id")
if fcomp == compartment_id:
return True
if fcomp is None:
return True # resource without compartment (IAM tenancy-level) → include
return False
def _filter_by_compartment(result, compartment_id):
passed = failed = 0
for check in result["checks"]:
original = check["findings"]
filtered = [f for f in original if _finding_in_compartment(f, compartment_id)]
check["findings"] = filtered
check["findings_count"] = len(filtered)
if check["status"] == "FAIL" and len(filtered) == 0:
check["status"] = "PASS"
if check["status"] == "PASS":
passed += 1
elif check["status"] == "FAIL":
failed += 1
total = passed + failed
result["passed"] = passed
result["failed"] = failed
result["compliance_score"] = round(passed / total * 100, 1) if total > 0 else 0
result["compartment_filter"] = compartment_id
# ──────────────────────────────────────────────────────────────────────
# Remediation Data
# ──────────────────────────────────────────────────────────────────────
REMEDIATIONS = {
"1.1": {"description": "Ensure service level admins are created to manage resources of particular service.", "steps": ["Create groups for each service (e.g., VolumeAdmins, ComputeAdmins)", "Write IAM policies granting each group manage access only to their service"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/policieshow/how-policies-work.htm"},
"1.2": {"description": "Ensure permissions on all resources are given only to the tenancy administrator group.", "steps": ["Review policies with 'manage all-resources in tenancy'", "Remove policies granting full access to non-Administrators groups"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/policieshow/how-policies-work.htm"},
"1.3": {"description": "Ensure IAM administrators cannot update tenancy Administrators group.", "steps": ["Add condition 'where target.group.name != Administrators' to IAM admin policies"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/policieshow/common-policies.htm"},
"1.4": {"description": "Ensure IAM password policy requires minimum length of 14 or greater.", "steps": ["Identity > Authentication Settings > Set minimum password length to 14+"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingpasswordrules.htm"},
"1.5": {"description": "Ensure IAM password policy expires passwords within 365 days.", "steps": ["Identity > Authentication Settings > Set expiration to 365 days or less"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingpasswordrules.htm"},
"1.6": {"description": "Ensure IAM password policy prevents password reuse.", "steps": ["Identity > Authentication Settings > Enable reuse prevention"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingpasswordrules.htm"},
"1.7": {"description": "Ensure MFA is enabled for all users with a console password.", "steps": ["Enable MFA for each user with console access", "Enable MFA enforcement in Authentication Settings"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/usingmfa.htm"},
"1.8": {"description": "Ensure user API keys rotate within 90 days.", "steps": ["Identify API keys older than 90 days", "Generate new keys, delete old ones"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingcredentials.htm"},
"1.9": {"description": "Ensure user customer secret keys rotate within 90 days.", "steps": ["Identify secret keys older than 90 days", "Rotate and delete old keys"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingcredentials.htm"},
"1.10": {"description": "Ensure user auth tokens rotate within 90 days.", "steps": ["Identify auth tokens older than 90 days", "Generate new, delete old"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingcredentials.htm"},
"1.11": {"description": "Ensure user IAM Database Passwords rotate within 90 days.", "steps": ["Identify DB passwords older than 90 days", "Reset passwords"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingcredentials.htm"},
"1.12": {"description": "Ensure API keys are not created for tenancy administrator users.", "steps": ["Remove API keys from Administrators group members", "Use instance principals for automation"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingcredentials.htm"},
"1.13": {"description": "Ensure all IAM users have a valid email address.", "steps": ["Review users and ensure valid email configured"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingusers.htm"},
"1.14": {"description": "Ensure Instance Principal authentication is used for instances, ADB and Functions.", "steps": ["Create dynamic groups", "Write policies for dynamic groups", "Update apps to use instance principals"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm"},
"1.15": {"description": "Ensure storage admins cannot delete resources they manage.", "steps": ["Add 'where request.permission != *_DELETE' conditions"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/policieshow/common-policies.htm"},
"1.16": {"description": "Ensure credentials unused for 45+ days are disabled.", "steps": ["Identify inactive users", "Disable accounts and credentials"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingusers.htm"},
"1.17": {"description": "Ensure only one active API Key per user.", "steps": ["Review users with multiple active API keys", "Remove extras"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingcredentials.htm"},
"2.1": {"description": "No security lists allow ingress from 0.0.0.0/0 to port 22.", "steps": ["Review security lists for SSH rules", "Remove or restrict 0.0.0.0/0 to port 22"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Network/Concepts/securitylists.htm"},
"2.2": {"description": "No security lists allow ingress from 0.0.0.0/0 to port 3389.", "steps": ["Remove or restrict 0.0.0.0/0 to port 3389"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Network/Concepts/securitylists.htm"},
"2.3": {"description": "No NSGs allow ingress from 0.0.0.0/0 to port 22.", "steps": ["Review and restrict NSG SSH rules"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Network/Concepts/networksecuritygroups.htm"},
"2.4": {"description": "No NSGs allow ingress from 0.0.0.0/0 to port 3389.", "steps": ["Review and restrict NSG RDP rules"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Network/Concepts/networksecuritygroups.htm"},
"2.5": {"description": "Default security list restricts all traffic except ICMP.", "steps": ["Remove permissive rules from default security list", "Allow only ICMP within VCN CIDR"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Network/Concepts/securitylists.htm"},
"2.6": {"description": "OIC access is restricted to allowed sources.", "steps": ["Configure OIC network access restrictions"], "oci_docs": "https://docs.oracle.com/en-us/iaas/integration/doc/restricting-network-access.html"},
"2.7": {"description": "OAC access is restricted or deployed within a VCN.", "steps": ["Deploy OAC with private access channel"], "oci_docs": "https://docs.oracle.com/en-us/iaas/analytics-cloud/doc/manage-network-access.html"},
"2.8": {"description": "ADB access is restricted or within a VCN.", "steps": ["Configure ADB with private endpoint"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Database/Concepts/adbsprivateaccess.htm"},
"3.1": {"description": "Compute Legacy Metadata endpoint is disabled.", "steps": ["Set 'are-legacy-imds-endpoints-disabled' to true"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Compute/Tasks/gettingmetadata.htm"},
"3.2": {"description": "Secure Boot is enabled on Compute Instance.", "steps": ["Enable Secure Boot in launch options"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Compute/References/shielded-instances.htm"},
"3.3": {"description": "In-transit Encryption is enabled on Compute Instance.", "steps": ["Enable in-transit encryption in launch options"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Block/Concepts/intransitencryption.htm"},
"4.1": {"description": "Default tags are used on resources.", "steps": ["Create tag defaults on root compartment"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Tagging/Tasks/managingtagdefaults.htm"},
"4.2": {"description": "At least one notification topic and subscription exists.", "steps": ["Create ONS topic + subscription"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Notification/Tasks/managingtopicsandsubscriptions.htm"},
"4.3": {"description": "Notification for Identity Provider changes.", "steps": ["Create Events rule for IdP CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
"4.4": {"description": "Notification for IdP group mapping changes.", "steps": ["Create Events rule for IdP mapping CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
"4.5": {"description": "Notification for IAM group changes.", "steps": ["Create Events rule for group CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
"4.6": {"description": "Notification for IAM policy changes.", "steps": ["Create Events rule for policy CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
"4.7": {"description": "Notification for user changes.", "steps": ["Create Events rule for user CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
"4.8": {"description": "Notification for VCN changes.", "steps": ["Create Events rule for VCN CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
"4.9": {"description": "Notification for route table changes.", "steps": ["Create Events rule for route table CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
"4.10": {"description": "Notification for security list changes.", "steps": ["Create Events rule for SL CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
"4.11": {"description": "Notification for NSG changes.", "steps": ["Create Events rule for NSG CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
"4.12": {"description": "Notification for network gateway changes.", "steps": ["Create Events rules for DRG/IGW/NGW/SGW CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
"4.13": {"description": "VCN flow logging is enabled for all subnets.", "steps": ["Enable VCN flow logs on each subnet"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Network/Concepts/vcn-flow-logs.htm"},
"4.14": {"description": "Cloud Guard is enabled in root compartment.", "steps": ["Enable Cloud Guard in root compartment"], "oci_docs": "https://docs.oracle.com/en-us/iaas/cloud-guard/using/index.htm"},
"4.15": {"description": "Notification for Cloud Guard problems.", "steps": ["Create Events rule for CG problems"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
"4.16": {"description": "CMK is rotated at least annually.", "steps": ["Rotate keys older than 365 days"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/KeyManagement/Tasks/managingkeys.htm"},
"4.17": {"description": "Write level Object Storage logging enabled.", "steps": ["Enable write logs for each bucket"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Logging/Concepts/service_logs.htm"},
"4.18": {"description": "Notification for Local OCI User Authentication.", "steps": ["Create Events rule for interactive login"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
"5.1.1": {"description": "No Object Storage buckets are publicly visible.", "steps": ["Set all buckets to NoPublicAccess"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Object/Tasks/managingbuckets.htm"},
"5.1.2": {"description": "Object Storage encrypted with CMK.", "steps": ["Assign CMK to each bucket"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Object/Tasks/encryption.htm"},
"5.1.3": {"description": "Versioning enabled for Object Storage.", "steps": ["Enable versioning on each bucket"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Object/Tasks/usingversioning.htm"},
"5.2.1": {"description": "Block Volumes encrypted with CMK.", "steps": ["Assign CMK to each block volume"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Block/Concepts/blockvolumeencryption.htm"},
"5.2.2": {"description": "Boot Volumes encrypted with CMK.", "steps": ["Assign CMK to each boot volume"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Block/Concepts/blockvolumeencryption.htm"},
"5.3.1": {"description": "File Storage encrypted with CMK.", "steps": ["Assign CMK to each file system"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/File/Concepts/filestorageoverview.htm"},
"6.1": {"description": "At least one compartment exists.", "steps": ["Create compartments for organizing resources"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingcompartments.htm"},
"6.2": {"description": "No resources in root compartment.", "steps": ["Move resources to child compartments"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingcompartments.htm"},
}
# Static checks catalog
CHECKS_CATALOG = {
"1.1": ("IAM-1", "Identity and Access Management", "Ensure service level admins are created to manage resources of particular service", 1),
"1.2": ("IAM-2", "Identity and Access Management", "Ensure permissions on all resources are given only to the tenancy administrator group", 1),
"1.3": ("IAM-3", "Identity and Access Management", "Ensure IAM administrators cannot update tenancy Administrators group", 1),
"1.4": ("IAM-4", "Identity and Access Management", "Ensure IAM password policy requires minimum length of 14 or greater", 1),
"1.5": ("IAM-5", "Identity and Access Management", "Ensure IAM password policy expires passwords within 365 days", 1),
"1.6": ("IAM-6", "Identity and Access Management", "Ensure IAM password policy prevents password reuse", 1),
"1.7": ("IAM-7", "Identity and Access Management", "Ensure MFA is enabled for all users with a console password", 1),
"1.8": ("IAM-8", "Identity and Access Management", "Ensure user API keys rotate within 90 days or less", 1),
"1.9": ("IAM-9", "Identity and Access Management", "Ensure user customer secret keys rotate within 90 days or less", 1),
"1.10": ("IAM-10", "Identity and Access Management", "Ensure user auth tokens rotate within 90 days or less", 1),
"1.11": ("IAM-11", "Identity and Access Management", "Ensure user IAM Database Passwords rotate within 90 days", 1),
"1.12": ("IAM-12", "Identity and Access Management", "Ensure API keys are not created for tenancy administrator users", 1),
"1.13": ("IAM-13", "Identity and Access Management", "Ensure all OCI IAM user accounts have a valid and current email address", 1),
"1.14": ("IAM-14", "Identity and Access Management", "Ensure Instance Principal authentication is used", 1),
"1.15": ("IAM-15", "Identity and Access Management", "Ensure storage service-level admins cannot delete resources they manage", 2),
"1.16": ("IAM-16", "Identity and Access Management", "Ensure OCI IAM credentials unused for 45 days or more are disabled", 1),
"1.17": ("IAM-17", "Identity and Access Management", "Ensure there is only one active API Key for any single OCI IAM user", 1),
"2.1": ("NTW-1", "Networking", "No security lists allow ingress from 0.0.0.0/0 to port 22", 1),
"2.2": ("NTW-2", "Networking", "No security lists allow ingress from 0.0.0.0/0 to port 3389", 1),
"2.3": ("NTW-3", "Networking", "No NSGs allow ingress from 0.0.0.0/0 to port 22", 1),
"2.4": ("NTW-4", "Networking", "No NSGs allow ingress from 0.0.0.0/0 to port 3389", 1),
"2.5": ("NTW-5", "Networking", "Default security list restricts all traffic except ICMP", 1),
"2.6": ("NTW-6", "Networking", "OIC access is restricted to allowed sources", 1),
"2.7": ("NTW-7", "Networking", "OAC access is restricted or deployed within a VCN", 1),
"2.8": ("NTW-8", "Networking", "ADB access is restricted or deployed within a VCN", 1),
"3.1": ("COM-1", "Compute", "Legacy Metadata service endpoint is disabled", 2),
"3.2": ("COM-2", "Compute", "Secure Boot is enabled on Compute Instance", 2),
"3.3": ("COM-3", "Compute", "In-transit Encryption is enabled on Compute Instance", 1),
"4.1": ("LAM-1", "Logging and Monitoring", "Default tags are used on resources", 1),
"4.2": ("LAM-2", "Logging and Monitoring", "At least one notification topic and subscription", 1),
"4.3": ("LAM-3", "Logging and Monitoring", "Notification for Identity Provider changes", 1),
"4.4": ("LAM-4", "Logging and Monitoring", "Notification for IdP group mapping changes", 1),
"4.5": ("LAM-5", "Logging and Monitoring", "Notification for IAM group changes", 1),
"4.6": ("LAM-6", "Logging and Monitoring", "Notification for IAM policy changes", 1),
"4.7": ("LAM-7", "Logging and Monitoring", "Notification for user changes", 1),
"4.8": ("LAM-8", "Logging and Monitoring", "Notification for VCN changes", 1),
"4.9": ("LAM-9", "Logging and Monitoring", "Notification for route table changes", 1),
"4.10": ("LAM-10", "Logging and Monitoring", "Notification for security list changes", 1),
"4.11": ("LAM-11", "Logging and Monitoring", "Notification for NSG changes", 1),
"4.12": ("LAM-12", "Logging and Monitoring", "Notification for network gateway changes", 1),
"4.13": ("LAM-13", "Logging and Monitoring", "VCN flow logging enabled for all subnets", 2),
"4.14": ("LAM-14", "Logging and Monitoring", "Cloud Guard enabled in root compartment", 1),
"4.15": ("LAM-15", "Logging and Monitoring", "Notification for Cloud Guard problems", 2),
"4.16": ("LAM-16", "Logging and Monitoring", "CMK rotated at least annually", 1),
"4.17": ("LAM-17", "Logging and Monitoring", "Write level Object Storage logging enabled", 2),
"4.18": ("LAM-18", "Logging and Monitoring", "Notification for Local OCI User Authentication", 1),
"5.1.1": ("STO-1-1", "Storage - Object Storage", "No Object Storage buckets publicly visible", 1),
"5.1.2": ("STO-1-2", "Storage - Object Storage", "Object Storage encrypted with CMK", 2),
"5.1.3": ("STO-1-3", "Storage - Object Storage", "Versioning enabled for Object Storage", 2),
"5.2.1": ("STO-2-1", "Storage - Block Volumes", "Block Volumes encrypted with CMK", 2),
"5.2.2": ("STO-2-2", "Storage - Block Volumes", "Boot Volumes encrypted with CMK", 2),
"5.3.1": ("STO-3-1", "Storage - File Storage Service", "File Storage encrypted with CMK", 2),
"6.1": ("AM-1", "Asset Management", "At least one compartment exists", 1),
"6.2": ("AM-2", "Asset Management", "No resources in root compartment", 1),
}
# ──────────────────────────────────────────────────────────────────────
# Tool Definitions
# ──────────────────────────────────────────────────────────────────────
TOOLS = [
Tool(name="cis_list_configs",
description="Lista as configurações OCI disponíveis. Não requer parâmetros.",
inputSchema={"type": "object", "properties": {}, "required": []}),
Tool(name="cis_list_checks",
description="Lista todos os 48 checks CIS disponíveis com ID, título, seção e nível. Estático, sem conexão OCI.",
inputSchema={"type": "object", "properties": {
"section": {"type": "string", "enum": ["iam", "networking", "compute", "logging_monitoring", "storage", "asset_management"], "description": "Filtrar por seção"},
"level": {"type": "integer", "enum": [1, 2], "description": "Filtrar por nível CIS"},
}, "required": []}),
Tool(name="cis_scan_iam",
description="Coleta dados IAM e executa checks CIS 1.1-1.17: políticas, usuários, MFA, API keys, passwords, dynamic groups. Rápido (~30s).",
inputSchema={"type": "object", "properties": {
"config_id": {"type": "string", "description": "ID da config OCI"},
"regions": {"type": "array", "items": {"type": "string"}, "description": "Regiões específicas (ex: ['us-ashburn-1']). Vazio = todas."},
"compartment_id": {"type": "string", "description": "OCID de compartment para filtrar findings. Vazio = todos os compartments."},
}, "required": ["config_id"]}),
Tool(name="cis_scan_networking",
description="Coleta dados de rede e executa checks CIS 2.1-2.8: security lists, NSGs, VCNs, subnets, OIC, OAC, ADB.",
inputSchema={"type": "object", "properties": {
"config_id": {"type": "string", "description": "ID da config OCI"},
"regions": {"type": "array", "items": {"type": "string"}, "description": "Regiões específicas (ex: ['us-ashburn-1']). Vazio = todas."},
"compartment_id": {"type": "string", "description": "OCID de compartment para filtrar findings. Vazio = todos os compartments."},
}, "required": ["config_id"]}),
Tool(name="cis_scan_compute",
description="Coleta instâncias Compute e executa checks CIS 3.1-3.3: metadata v2, secure boot, in-transit encryption. Rápido.",
inputSchema={"type": "object", "properties": {
"config_id": {"type": "string", "description": "ID da config OCI"},
"regions": {"type": "array", "items": {"type": "string"}, "description": "Regiões específicas (ex: ['us-ashburn-1']). Vazio = todas."},
"compartment_id": {"type": "string", "description": "OCID de compartment para filtrar findings. Vazio = todos os compartments."},
}, "required": ["config_id"]}),
Tool(name="cis_scan_logging_monitoring",
description="Coleta dados de logging/monitoring e executa checks CIS 4.1-4.18: tags, notificações, events, Cloud Guard, KMS, logs.",
inputSchema={"type": "object", "properties": {
"config_id": {"type": "string", "description": "ID da config OCI"},
"regions": {"type": "array", "items": {"type": "string"}, "description": "Regiões específicas (ex: ['us-ashburn-1']). Vazio = todas."},
"compartment_id": {"type": "string", "description": "OCID de compartment para filtrar findings. Vazio = todos os compartments."},
}, "required": ["config_id"]}),
Tool(name="cis_scan_storage",
description="Coleta dados de storage e executa checks CIS 5.1.1-5.3.1: buckets, block/boot volumes, file storage, CMK, versioning.",
inputSchema={"type": "object", "properties": {
"config_id": {"type": "string", "description": "ID da config OCI"},
"regions": {"type": "array", "items": {"type": "string"}, "description": "Regiões específicas (ex: ['us-ashburn-1']). Vazio = todas."},
"compartment_id": {"type": "string", "description": "OCID de compartment para filtrar findings. Vazio = todos os compartments."},
}, "required": ["config_id"]}),
Tool(name="cis_scan_asset_management",
description="Coleta dados de assets e executa checks CIS 6.1-6.2: compartments, recursos no root.",
inputSchema={"type": "object", "properties": {
"config_id": {"type": "string", "description": "ID da config OCI"},
"regions": {"type": "array", "items": {"type": "string"}, "description": "Regiões específicas (ex: ['us-ashburn-1']). Vazio = todas."},
"compartment_id": {"type": "string", "description": "OCID de compartment para filtrar findings. Vazio = todos os compartments."},
}, "required": ["config_id"]}),
Tool(name="cis_get_check",
description="Retorna findings detalhados de um check CIS individual por ID (ex: '1.7', '2.1'). Requer scan da seção correspondente.",
inputSchema={"type": "object", "properties": {
"config_id": {"type": "string", "description": "ID da config OCI"},
"check_id": {"type": "string", "description": "ID do check CIS (ex: '1.7')"},
"regions": {"type": "array", "items": {"type": "string"}, "description": "Regiões específicas (ex: ['us-ashburn-1']). Vazio = todas."},
}, "required": ["config_id", "check_id"]}),
Tool(name="cis_get_remediation",
description="Orientação de remediação para um check CIS: descrição, passos, link docs Oracle. Estático, sem conexão OCI.",
inputSchema={"type": "object", "properties": {
"check_id": {"type": "string", "description": "ID do check CIS (ex: '1.7')"},
}, "required": ["check_id"]}),
Tool(name="cis_get_scan_status",
description="Status da sessão: quais seções já foram coletadas/analisadas, TTL restante, regiões filtradas.",
inputSchema={"type": "object", "properties": {
"config_id": {"type": "string", "description": "ID da config OCI"},
"regions": {"type": "array", "items": {"type": "string"}, "description": "Regiões da sessão a consultar. Vazio = sessão sem filtro."},
}, "required": ["config_id"]}),
Tool(name="cis_invalidate_cache",
description="Limpa cache de sessão. Próximo scan coletará dados novamente.",
inputSchema={"type": "object", "properties": {
"config_id": {"type": "string", "description": "ID da config OCI"},
"regions": {"type": "array", "items": {"type": "string"}, "description": "Regiões da sessão a limpar. Vazio = sessão sem filtro."},
}, "required": ["config_id"]}),
]
# ──────────────────────────────────────────────────────────────────────
# Handlers
# ──────────────────────────────────────────────────────────────────────
def _handle_cis_list_configs(args):
configs = _list_available_configs()
return {"configs": configs, "total": len(configs)}
def _handle_cis_list_checks(args):
sf = args.get("section"); lf = args.get("level")
results = []
for rec_id, (cid, sec, title, lvl) in CHECKS_CATALOG.items():
if sf and _check_id_to_section(rec_id) != sf: continue
if lf and lvl != lf: continue
results.append({"check_id": rec_id, "id": cid, "section": sec, "title": title, "level": lvl})
return {"checks": results, "total": len(results)}
def _handle_scan_section(args, section: str):
config_id = args["config_id"]
regions = args.get("regions") or None
compartment_id = args.get("compartment_id") or None
session = _get_or_create_checker(config_id, regions=regions)
_analyze_section(session, section)
result = _section_summary(session["checker"], section)
if regions:
result["regions_filter"] = regions
if compartment_id:
_filter_by_compartment(result, compartment_id)
col_errors = session.get("collection_errors", {}).get(section, [])
if col_errors:
result["partial_errors"] = col_errors
result["warning"] = f"{len(col_errors)} coletores falharam — resultado pode estar incompleto"
return result
def _handle_cis_get_check(args):
config_id = args["config_id"]; check_id = args["check_id"]
regions = args.get("regions") or None
section = _check_id_to_section(check_id)
if section == "unknown":
return {"error": f"Check '{check_id}' não encontrado. Use cis_list_checks."}
session = _get_or_create_checker(config_id, regions=regions)
_analyze_section(session, section)
benchmark = _get_benchmark(session["checker"])
if check_id not in benchmark:
return {"error": f"Check '{check_id}' não encontrado."}
check = benchmark[check_id]
report_data = session["checker"].cis_report_data.get(check_id, {})
return {
"check_id": check_id, "id": check["id"], "title": check["Title"], "section": check["section"],
"status": "PASS" if check["Status"] is True else ("FAIL" if check["Status"] is False else "N/A"),
"level": check.get("Level", "N/A"),
"total_resources": len(check.get("Total", [])), "findings_count": len(check.get("Findings", [])),
"findings": _serialize_oci_obj(check.get("Findings", [])[:30]),
"description": report_data.get("Description", ""),
"remediation_guidance": report_data.get("Remediation", ""),
}
def _handle_cis_get_remediation(args):
cid = args["check_id"]
if cid not in REMEDIATIONS:
return {"error": f"Check '{cid}' não encontrado."}
return {"check_id": cid, **REMEDIATIONS[cid]}
def _handle_cis_get_scan_status(args):
config_id = args["config_id"]
regions = args.get("regions") or None
key = _session_key(config_id, regions)
if key not in _sessions:
return {"config_id": config_id, "regions": regions, "status": "not_initialized", "sections_collected": [], "sections_analyzed": []}
s = _sessions[key]
elapsed = time.time() - s["created_at"]
return {
"config_id": config_id, "regions": s.get("regions"), "status": "active",
"base_collected": s["base_collected"],
"sections_collected": list(s["sections_collected"]),
"sections_analyzed": list(s["sections_analyzed"]),
"session_age_seconds": round(elapsed),
"ttl_remaining_seconds": round(max(0, SESSION_TTL - elapsed)),
}
def _handle_cis_invalidate_cache(args):
config_id = args["config_id"]
regions = args.get("regions") or None
key = _session_key(config_id, regions)
if key in _sessions:
del _sessions[key]
return {"status": "cleared", "config_id": config_id, "regions": regions}
return {"status": "not_found", "config_id": config_id, "regions": regions}
TOOL_HANDLERS = {
"cis_list_configs": _handle_cis_list_configs,
"cis_list_checks": _handle_cis_list_checks,
"cis_scan_iam": lambda a: _handle_scan_section(a, "iam"),
"cis_scan_networking": lambda a: _handle_scan_section(a, "networking"),
"cis_scan_compute": lambda a: _handle_scan_section(a, "compute"),
"cis_scan_logging_monitoring": lambda a: _handle_scan_section(a, "logging_monitoring"),
"cis_scan_storage": lambda a: _handle_scan_section(a, "storage"),
"cis_scan_asset_management": lambda a: _handle_scan_section(a, "asset_management"),
"cis_get_check": _handle_cis_get_check,
"cis_get_remediation": _handle_cis_get_remediation,
"cis_get_scan_status": _handle_cis_get_scan_status,
"cis_invalidate_cache": _handle_cis_invalidate_cache,
}
# ──────────────────────────────────────────────────────────────────────
# MCP Interface
# ──────────────────────────────────────────────────────────────────────
@server.list_tools()
async def list_tools() -> list[Tool]:
return TOOLS
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
handler = TOOL_HANDLERS.get(name)
if not handler:
return [TextContent(type="text", text=json.dumps({"error": f"Tool '{name}' not found"}))]
last_error = None
loop = asyncio.get_event_loop()
for attempt in range(1 + TOOL_RETRIES):
try:
result = await asyncio.wait_for(
loop.run_in_executor(None, partial(handler, arguments)),
timeout=TOOL_TIMEOUT
)
return [TextContent(type="text", text=json.dumps(result, default=str, ensure_ascii=False))]
except asyncio.TimeoutError:
last_error = f"Tool '{name}' timed out after {TOOL_TIMEOUT}s"
except Exception as e:
last_error = f"{e}"
if attempt < TOOL_RETRIES:
print(f"[Retry] Tool '{name}' failed ({last_error}), retrying...")
return [TextContent(type="text", text=json.dumps({"error": last_error}))]
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, server.create_initialization_options())
if __name__ == "__main__":
asyncio.run(main())