diff --git a/extensions/business/cybersec/red_mesh/pentester_api_01.py b/extensions/business/cybersec/red_mesh/pentester_api_01.py index dbba0e31..082f59a0 100644 --- a/extensions/business/cybersec/red_mesh/pentester_api_01.py +++ b/extensions/business/cybersec/red_mesh/pentester_api_01.py @@ -30,8 +30,11 @@ """ +import ipaddress import random +from urllib.parse import urlparse + from naeural_core.business.default.web_app.fast_api_web_app import FastApiWebAppPlugin as BasePlugin from .redmesh_utils import PentestLocalWorker # Import PentestJob from separate module from .redmesh_llm_agent_mixin import _RedMeshLlmAgentMixin @@ -42,7 +45,7 @@ LLM_ANALYSIS_REMEDIATION_PLAN, ) -__VER__ = '0.8.2' +__VER__ = '0.9.0' _CONFIG = { **BasePlugin.CONFIG, @@ -82,6 +85,10 @@ "LLM_AGENT_API_TIMEOUT": 120, # Timeout in seconds for LLM API calls "LLM_AUTO_ANALYSIS_TYPE": "security_assessment", # Default analysis type + # RedMesh attestation submission + "ATTESTATION_ENABLED": True, + "ATTESTATION_MIN_SECONDS_BETWEEN_SUBMITS": 86400, + 'VALIDATION_RULES': { **BasePlugin.CONFIG['VALIDATION_RULES'], }, @@ -108,6 +115,7 @@ class PentesterApi01Plugin(BasePlugin, _RedMeshLlmAgentMixin): List of job_ids that completed locally (used for status responses). """ CONFIG = _CONFIG + REDMESH_ATTESTATION_DOMAIN = "0xced141225d43c56d8b224d12f0b9524a15dc86df0113c42ffa4bc859309e0d40" def on_init(self): @@ -198,6 +206,130 @@ def Pd(self, s, *args, score=-1, **kwargs): return + def _attestation_get_tenant_private_key(self): + env_name = "R1EN_ATTESTATION_PRIVATE_KEY" + private_key = self.os_environ.get(env_name, None) + if private_key: + private_key = private_key.strip() + if not private_key: + return None + return private_key + + @staticmethod + def _attestation_pack_cid_obfuscated(report_cid) -> str: + if not isinstance(report_cid, str) or len(report_cid.strip()) == 0: + return "0x" + ("00" * 10) + cid = report_cid.strip() + if len(cid) >= 10: + masked = cid[:5] + cid[-5:] + else: + masked = cid.ljust(10, "_") + safe = "".join(ch if 32 <= ord(ch) <= 126 else "_" for ch in masked)[:10] + data = safe.encode("ascii", errors="ignore") + if len(data) < 10: + data = data + (b"_" * (10 - len(data))) + return "0x" + data[:10].hex() + + @staticmethod + def _attestation_extract_host(target): + if not isinstance(target, str): + return None + target = target.strip() + if not target: + return None + if "://" in target: + parsed = urlparse(target) + if parsed.hostname: + return parsed.hostname + host = target.split("/", 1)[0] + if host.count(":") == 1 and "." in host: + host = host.split(":", 1)[0] + return host + + def _attestation_pack_ip_obfuscated(self, target) -> str: + host = self._attestation_extract_host(target) + if not host: + return "0x0000" + if ".." in host: + parts = host.split("..") + if len(parts) == 2 and all(part.isdigit() for part in parts): + first_octet = int(parts[0]) + last_octet = int(parts[1]) + if 0 <= first_octet <= 255 and 0 <= last_octet <= 255: + return f"0x{first_octet:02x}{last_octet:02x}" + try: + ip_obj = ipaddress.ip_address(host) + except Exception: + return "0x0000" + if ip_obj.version != 4: + return "0x0000" + octets = host.split(".") + first_octet = int(octets[0]) + last_octet = int(octets[-1]) + return f"0x{first_octet:02x}{last_octet:02x}" + + + def _submit_attestation(self, job_id: str, job_specs: dict, workers: dict): + if not self.cfg_attestation_enabled: + return None + tenant_private_key = self._attestation_get_tenant_private_key() + if tenant_private_key is None: + self.P( + "RedMesh attestation is enabled but tenant private key is missing. " + "Expected env var 'R1EN_ATTESTATION_PRIVATE_KEY'.", + color='y' + ) + return None + + run_mode = str(job_specs.get("run_mode", "SINGLEPASS")).upper() + test_mode = 1 if run_mode == "CONTINUOUS_MONITORING" else 0 + node_count = len(workers) if isinstance(workers, dict) else 0 + # TODO: replace placeholder score with proper RedMesh vulnerability scoring logic. + vulnerability_score = 100 + target = job_specs.get("target") + report_cid = workers.get(self.ee_addr, {}).get("report_cid", None) #TODO: use the correct CID + node_eth_address = self.bc.eth_address + ip_obfuscated = self._attestation_pack_ip_obfuscated(target) + cid_obfuscated = self._attestation_pack_cid_obfuscated(report_cid) + + tx_hash = self.bc.submit_attestation( + function_name="submitRedmeshAttestation", + function_args=[ + test_mode, + node_count, + vulnerability_score, + ip_obfuscated, + cid_obfuscated, + ], + signature_types=["bytes32", "uint8", "uint16", "uint8", "bytes2", "bytes10"], + signature_values=[ + self.REDMESH_ATTESTATION_DOMAIN, + test_mode, + node_count, + vulnerability_score, + ip_obfuscated, + cid_obfuscated, + ], + tx_private_key=tenant_private_key, + ) + + result = { + "job_id": job_id, + "tx_hash": tx_hash, + "test_mode": "C" if test_mode == 1 else "S", + "node_count": node_count, + "vulnerability_score": vulnerability_score, + "report_cid": report_cid, + "node_eth_address": node_eth_address, + } + self.P( + "Submitted RedMesh attestation for " + f"{job_id} (tx: {tx_hash}, node: {node_eth_address}, score: {vulnerability_score})", + color='g' + ) + return result + + def __post_init(self): """ Perform warmup: reconcile existing jobs in CStore, migrate legacy keys, @@ -848,11 +980,38 @@ def _maybe_finalize_pass(self): # ═══════════════════════════════════════════════════ # STATE: All peers completed current pass # ═══════════════════════════════════════════════════ - pass_history.append({ + now_ts = self.time() + pass_record = { "pass_nr": job_pass, - "completed_at": self.time(), + "completed_at": now_ts, "reports": {addr: w.get("report_cid") for addr, w in workers.items()} - }) + } + + should_submit_attestation = True + if run_mode == "CONTINUOUS_MONITORING": + last_attestation_at = job_specs.get("last_attestation_at") + min_interval = self.cfg_attestation_min_seconds_between_submits + if last_attestation_at is not None and now_ts - last_attestation_at < min_interval: + should_submit_attestation = False + + if should_submit_attestation: + # Best-effort on-chain summary; failures must not block pass finalization. + try: + redmesh_attestation = self._submit_attestation( + job_id=job_id, + job_specs=job_specs, + workers=workers + ) + if redmesh_attestation is not None: + pass_record["redmesh_attestation"] = redmesh_attestation + job_specs["last_attestation_at"] = now_ts + except Exception as exc: + self.P( + f"Failed to submit RedMesh attestation for job {job_id}: {exc}", + color='r' + ) + + pass_history.append(pass_record) # Handle SINGLEPASS - set FINALIZED and exit (no scheduling) if run_mode == "SINGLEPASS":