From 2628219d85a62f5ae4d4646409a804883ddbea7c Mon Sep 17 00:00:00 2001 From: Alessandro Date: Thu, 5 Mar 2026 10:07:50 +0100 Subject: [PATCH 1/3] feat: add attestation registry submission in redmesh close flow --- .../cybersec/red_mesh/pentester_api_01.py | 167 +++++++++++++++++- 1 file changed, 163 insertions(+), 4 deletions(-) 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": From 5fb785925cc81e0cf00f61428076eace8280233b Mon Sep 17 00:00:00 2001 From: Alessandro Date: Thu, 5 Mar 2026 10:27:30 +0100 Subject: [PATCH 2/3] fix: add execution_id to attestation --- .../cybersec/red_mesh/pentester_api_01.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/extensions/business/cybersec/red_mesh/pentester_api_01.py b/extensions/business/cybersec/red_mesh/pentester_api_01.py index 082f59a0..9cbba4b7 100644 --- a/extensions/business/cybersec/red_mesh/pentester_api_01.py +++ b/extensions/business/cybersec/red_mesh/pentester_api_01.py @@ -268,6 +268,19 @@ def _attestation_pack_ip_obfuscated(self, target) -> str: last_octet = int(octets[-1]) return f"0x{first_octet:02x}{last_octet:02x}" + @staticmethod + def _attestation_pack_execution_id(job_id) -> str: + if not isinstance(job_id, str): + raise ValueError("job_id must be a string") + job_id = job_id.strip() + if len(job_id) != 8: + raise ValueError("job_id must be exactly 8 characters") + try: + data = job_id.encode("ascii") + except UnicodeEncodeError as exc: + raise ValueError("job_id must contain only ASCII characters") from exc + return "0x" + data.hex() + def _submit_attestation(self, job_id: str, job_specs: dict, workers: dict): if not self.cfg_attestation_enabled: @@ -287,6 +300,7 @@ def _submit_attestation(self, job_id: str, job_specs: dict, workers: dict): # TODO: replace placeholder score with proper RedMesh vulnerability scoring logic. vulnerability_score = 100 target = job_specs.get("target") + execution_id = self._attestation_pack_execution_id(job_id) 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) @@ -298,15 +312,17 @@ def _submit_attestation(self, job_id: str, job_specs: dict, workers: dict): test_mode, node_count, vulnerability_score, + execution_id, ip_obfuscated, cid_obfuscated, ], - signature_types=["bytes32", "uint8", "uint16", "uint8", "bytes2", "bytes10"], + signature_types=["bytes32", "uint8", "uint16", "uint8", "bytes8", "bytes2", "bytes10"], signature_values=[ self.REDMESH_ATTESTATION_DOMAIN, test_mode, node_count, vulnerability_score, + execution_id, ip_obfuscated, cid_obfuscated, ], @@ -319,6 +335,7 @@ def _submit_attestation(self, job_id: str, job_specs: dict, workers: dict): "test_mode": "C" if test_mode == 1 else "S", "node_count": node_count, "vulnerability_score": vulnerability_score, + "execution_id": execution_id, "report_cid": report_cid, "node_eth_address": node_eth_address, } From 5e98fd866654e0abdaef813f5bb28238f259565d Mon Sep 17 00:00:00 2001 From: Alessandro Date: Thu, 5 Mar 2026 11:43:50 +0100 Subject: [PATCH 3/3] Add RedMesh job-start attestation submission flow --- .../cybersec/red_mesh/pentester_api_01.py | 109 ++++++++++++++++-- 1 file changed, 102 insertions(+), 7 deletions(-) diff --git a/extensions/business/cybersec/red_mesh/pentester_api_01.py b/extensions/business/cybersec/red_mesh/pentester_api_01.py index 9cbba4b7..ce779565 100644 --- a/extensions/business/cybersec/red_mesh/pentester_api_01.py +++ b/extensions/business/cybersec/red_mesh/pentester_api_01.py @@ -282,7 +282,28 @@ def _attestation_pack_execution_id(job_id) -> str: return "0x" + data.hex() - def _submit_attestation(self, job_id: str, job_specs: dict, workers: dict): + def _attestation_get_worker_eth_addresses(self, workers: dict) -> list[str]: + if not isinstance(workers, dict): + return [] + eth_addresses = [] + for node_addr in workers.keys(): + eth_addr = self.bc.node_addr_to_eth_addr(node_addr) + if not isinstance(eth_addr, str) or not eth_addr.startswith("0x"): + raise ValueError(f"Unable to convert worker node to EVM address: {node_addr}") + eth_addresses.append(eth_addr) + eth_addresses.sort() + return eth_addresses + + def _attestation_pack_node_hashes(self, workers: dict) -> str: + eth_addresses = self._attestation_get_worker_eth_addresses(workers) + if len(eth_addresses) == 0: + return "0x" + ("00" * 32) + digest = self.bc.eth_hash_message(types=["address[]"], values=[eth_addresses], as_hex=True) + if isinstance(digest, str) and digest.startswith("0x"): + return digest + return "0x" + str(digest) + + def _submit_redmesh_test_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() @@ -307,7 +328,7 @@ def _submit_attestation(self, job_id: str, job_specs: dict, workers: dict): cid_obfuscated = self._attestation_pack_cid_obfuscated(report_cid) tx_hash = self.bc.submit_attestation( - function_name="submitRedmeshAttestation", + function_name="submitRedmeshTestAttestation", function_args=[ test_mode, node_count, @@ -340,12 +361,71 @@ def _submit_attestation(self, job_id: str, job_specs: dict, workers: dict): "node_eth_address": node_eth_address, } self.P( - "Submitted RedMesh attestation for " + "Submitted RedMesh test attestation for " f"{job_id} (tx: {tx_hash}, node: {node_eth_address}, score: {vulnerability_score})", color='g' ) return result + def _submit_redmesh_job_start_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 + target = job_specs.get("target") + execution_id = self._attestation_pack_execution_id(job_id) + node_eth_address = self.bc.eth_address + ip_obfuscated = self._attestation_pack_ip_obfuscated(target) + node_hashes = self._attestation_pack_node_hashes(workers) + + tx_hash = self.bc.submit_attestation( + function_name="submitRedmeshJobStartAttestation", + function_args=[ + test_mode, + node_count, + execution_id, + node_hashes, + ip_obfuscated, + ], + signature_types=["bytes32", "uint8", "uint16", "bytes8", "bytes32", "bytes2"], + signature_values=[ + self.REDMESH_ATTESTATION_DOMAIN, + test_mode, + node_count, + execution_id, + node_hashes, + ip_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, + "execution_id": execution_id, + "node_hashes": node_hashes, + "ip_obfuscated": ip_obfuscated, + "node_eth_address": node_eth_address, + } + self.P( + "Submitted RedMesh job-start attestation for " + f"{job_id} (tx: {tx_hash}, node: {node_eth_address}, node_count: {node_count})", + color='g' + ) + return result + def __post_init(self): """ @@ -1014,17 +1094,17 @@ def _maybe_finalize_pass(self): if should_submit_attestation: # Best-effort on-chain summary; failures must not block pass finalization. try: - redmesh_attestation = self._submit_attestation( + redmesh_test_attestation = self._submit_redmesh_test_attestation( job_id=job_id, job_specs=job_specs, workers=workers ) - if redmesh_attestation is not None: - pass_record["redmesh_attestation"] = redmesh_attestation + if redmesh_test_attestation is not None: + pass_record["redmesh_test_attestation"] = redmesh_test_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}", + f"Failed to submit RedMesh test attestation for job {job_id}: {exc}", color='r' ) @@ -1460,6 +1540,21 @@ def launch_test( # Peer selection (defaults to all chainstore_peers if not specified) "selected_peers": active_peers, } + + try: + redmesh_job_start_attestation = self._submit_redmesh_job_start_attestation( + job_id=job_id, + job_specs=job_specs, + workers=workers, + ) + if redmesh_job_start_attestation is not None: + job_specs["redmesh_job_start_attestation"] = redmesh_job_start_attestation + except Exception as exc: + self.P( + f"Failed to submit RedMesh job-start attestation for job {job_id}: {exc}", + color='r' + ) + self.chainstore_hset( hkey=self.cfg_instance_id, key=job_id,