Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -695,3 +695,30 @@ Entry format:
- Details: `ThHfModelBase` keeps Transformers/PT as the default GPU and fallback path, but CPU-only `HF_RUNTIME=auto` now loads `artifact_manifest.json`, selects a declared ONNX Runtime artifact, downloads only safe allow-patterns, loads schema and contract decoder from HF artifacts, and exposes the decoded artifact contract through the existing text-classifier flow. Business API response shaping now passes through generic model/runtime metadata emitted by serving.
- Verification: `python3 -m unittest extensions.serving.test_th_hf_model_base extensions.serving.test_th_text_classifier extensions.serving.test_th_privacy_filter extensions.business.edge_inference_api.test_text_classifier_inference_api extensions.business.edge_inference_api.test_privacy_filter_inference_api`; `python3 -m py_compile extensions/serving/default_inference/nlp/th_hf_model_base.py extensions/business/edge_inference_api/text_classifier_inference_api.py`; required serving gate `python3 -m unittest extensions.serving.model_testing.test_llm_servings` currently fails at import with `ImportError: cannot import name 'Logger' from 'naeural_core'`.
- Links: `extensions/serving/default_inference/nlp/th_hf_model_base.py`, `extensions/business/edge_inference_api/text_classifier_inference_api.py`, `extensions/serving/test_th_hf_model_base.py`

- ID: `ML-20260723-001`
- Timestamp: `2026-07-23T13:45:20Z`
- Type: `change`
- Summary: dAuth job-secret requests now require signed 120-second timestamp nonces, and GET responses encrypt secret bundles to the authorized runner.
- Criticality: Security protocol change preventing indefinite signed-request/response replay and removing plaintext job secrets from HTTP responses.
- Details: `/add_secrets` and `/get_secrets` validate signed hex-millisecond timestamp nonces and echo them in successful signed responses. `/get_secrets` encrypts the serialized bundle to the signed requester address; clients must verify the response signer and echoed nonce before decrypting.
- Verification: `python -m unittest discover -s extensions/business/dauth -p 'test_*.py'`; cross-repo SDK dAuth client tests.
- Links: `extensions/business/dauth/dauth_mixin.py`, `extensions/business/dauth/dauth_manager.py`

- ID: `ML-20260731-001`
- Timestamp: `2026-07-31T16:48:11Z`
- Type: `change`
- Summary: dAuth job-secret ChainStore writes and minute syncs now target only startup-cached dAuth registry peers.
- Criticality: Secret-replication boundary and recovery behavior across every dAuth server.
- Details: The dAuth manager reads registry ETH addresses once at startup, keeps local service eligibility fixed until restart, and refreshes only ETH-to-internal mappings from local NetMon state. `DAUTH_JOB_SECRETS` writes and 60-second hsync calls disable default/configured ChainStore peers. Known deferred risks: generic ChainStore does not authorize inbound operations by hash namespace, and first-response hsync has no freshness arbitration; production hardening requires an inbound ACL or dedicated authenticated replication protocol plus version-aware merges.
- Verification: `python3 -m unittest discover -s extensions/business/dauth -p 'test_*.py'`; `python3 -m py_compile extensions/business/dauth/dauth_registry.py extensions/business/dauth/dauth_manager.py extensions/business/dauth/dauth_mixin.py extensions/business/dauth/test_dauth_registry_gating.py extensions/business/dauth/test_dauth_secret_routing.py`; `git diff --check`
- Links: `extensions/business/dauth/dauth_registry.py`, `extensions/business/dauth/dauth_manager.py`, `extensions/business/dauth/dauth_mixin.py`

- ID: `ML-20260803-001`
- Timestamp: `2026-08-03T16:09:26Z`
- Type: `change`
- Summary: dAuth server eligibility and secret-replication peers now refresh from the on-chain registry every hour; secret hsync runs every 10 minutes.
- Criticality: Authorization revocation and secret-replication routing across every dAuth server.
- Details: Lifecycle pause/resume predicates perform the rate-limited registry refresh without adding RPC calls to endpoint request paths. Successful reads remain cached for one hour; failed reads clear cached peers, fail closed, and retry after one minute. Registry reads are synchronous and rely on the SDK Web3 provider to return or time out. A removed local node causes the web app to pause and become unready; readiness returns only after a resumed Uvicorn process reports startup. Remaining dAuth nodes replace their cached peer set on their next hourly refresh. The inbound namespace authorization and version-aware hsync limitations from `ML-20260731-001` remain open.
- Verification: `python3 -m unittest discover -s extensions/business/dauth -p 'test_*.py'`; `python3 -m py_compile extensions/business/dauth/dauth_registry.py extensions/business/dauth/dauth_manager.py extensions/business/dauth/dauth_mixin.py extensions/business/dauth/test_dauth_registry_gating.py extensions/business/dauth/test_dauth_secret_routing.py`; `git diff --check`
- Links: `extensions/business/dauth/dauth_manager.py`, `extensions/business/dauth/test_dauth_registry_gating.py`
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ For further information, visit our website at [https://ratio1.ai](https://ratio1

## Project Financing Disclaimer

This project incorporates open-source components developed with the support of financing grants **SMIS 143488** and **SMIS 156084**, provided by the Romanian Competitiveness Operational Programme. We extend our gratitude for this support, which has been instrumental in advancing our work and enabling us to share these resources with the community.
This project incorporates open-source components developed with the support of financing grants **SOLIS SMIS 143488** and **ReDeN SMIS 156084**, provided by the Romanian Competitiveness Operational Programme. We extend our gratitude for this support, which has been instrumental in advancing our work and enabling us to share these resources with the community.

The content and information within this repository reflect the authors' views and do not necessarily represent those of the funding agencies. The grants have specifically supported certain aspects of this open-source project, facilitating broader dissemination and collaborative development.

Expand Down
98 changes: 98 additions & 0 deletions extensions/business/cybersec/red_mesh/connection_metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Connection-window aggregation and signal semantics shared across scan levels."""

RESPONSIVE_CONNECTION_OUTCOMES = frozenset(("connected", "refused", "reset"))
MIN_QUALIFIED_WINDOW_ATTEMPTS = 5
BLOCKING_BASELINE_RATE = 0.8
BLOCKING_RESPONSE_RATE = 0.2
THROTTLING_DROP_RATIO = 0.7


def detect_connection_signals(windows: list | None) -> dict:
"""Derive blocking/throttling signals from sufficiently sampled windows."""
qualified = []
for window in windows or []:
attempts = window.get("attempts")
if not isinstance(attempts, (int, float)) or attempts < MIN_QUALIFIED_WINDOW_ATTEMPTS:
continue
responsive_count = window.get("responsive_count")
response_rate = window.get("response_rate")
if response_rate is None and responsive_count is not None:
response_rate = responsive_count / attempts
if responsive_count is None and response_rate is not None:
responsive_count = response_rate * attempts
if response_rate is None or responsive_count is None:
continue
qualified.append({
"attempts": attempts,
"responsive_count": responsive_count,
"response_rate": response_rate,
})

blocking = any(
previous["response_rate"] >= BLOCKING_BASELINE_RATE
and current["response_rate"] <= BLOCKING_RESPONSE_RATE
for previous, current in zip(qualified, qualified[1:])
)

throttling = False
if len(qualified) >= 4:
first_attempts = sum(window["attempts"] for window in qualified[:2])
last_attempts = sum(window["attempts"] for window in qualified[-2:])
first_responsive = sum(window["responsive_count"] for window in qualified[:2])
last_responsive = sum(window["responsive_count"] for window in qualified[-2:])
baseline_rate = first_responsive / first_attempts
later_rate = last_responsive / last_attempts
throttling = (
later_rate > BLOCKING_RESPONSE_RATE
and later_rate < baseline_rate * THROTTLING_DROP_RATIO
)

return {
"rate_limiting_detected": throttling,
"blocking_detected": blocking,
}


def merge_connection_windows(metrics_list: list) -> list | None:
"""Merge aligned count-bearing windows, excluding unverifiable legacy samples."""
grouped = {}
legacy_fallback = None
for metrics in metrics_list:
windows = metrics.get("success_rate_over_time") or []
if legacy_fallback is None or len(windows) > len(legacy_fallback):
legacy_fallback = windows
for window in windows:
attempts = window.get("attempts")
responsive_count = window.get("responsive_count")
if attempts is None or attempts <= 0:
continue
if responsive_count is None:
response_rate = window.get("response_rate")
if response_rate is None:
continue
responsive_count = round(response_rate * attempts)
key = (window.get("window_start", 0), window.get("window_end", 0))
bucket = grouped.setdefault(key, {
"attempts": 0,
"responsive_count": 0,
"connected_weight": 0.0,
})
bucket["attempts"] += attempts
bucket["responsive_count"] += responsive_count
bucket["connected_weight"] += window.get("success_rate", 0) * attempts

if not grouped:
return legacy_fallback or None

merged = []
for (window_start, window_end), counts in sorted(grouped.items()):
attempts = counts["attempts"]
merged.append({
"window_start": window_start,
"window_end": window_end,
"success_rate": round(counts["connected_weight"] / attempts, 3),
"attempts": attempts,
"responsive_count": counts["responsive_count"],
"response_rate": round(counts["responsive_count"] / attempts, 3),
})
return merged
41 changes: 41 additions & 0 deletions extensions/business/cybersec/red_mesh/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,25 @@ class ScanType(str, Enum):
PORT_ORDER_SHUFFLE = "SHUFFLE"
PORT_ORDER_SEQUENTIAL = "SEQUENTIAL"

# Network target-response timeout profiles. Standard preserves every existing
# call-site timeout; Thorough expands ordinary waits without changing probe
# breadth, pacing, or timing-sensitive detection thresholds.
TIMEOUT_PROFILE_STANDARD = "STANDARD"
TIMEOUT_PROFILE_THOROUGH = "THOROUGH"
TIMEOUT_PROFILES = frozenset({TIMEOUT_PROFILE_STANDARD, TIMEOUT_PROFILE_THOROUGH})


def normalize_timeout_profile(value):
normalized = str(value or TIMEOUT_PROFILE_STANDARD).strip().upper()
return normalized if normalized in TIMEOUT_PROFILES else TIMEOUT_PROFILE_STANDARD


def resolve_target_response_timeout(timeout_profile, standard_timeout):
"""Resolve an ordinary network target-response maximum wait in seconds."""
if normalize_timeout_profile(timeout_profile) != TIMEOUT_PROFILE_THOROUGH:
return standard_timeout
return round(min(float(standard_timeout) * 3, 15.0), 3)

# LLM Agent API status constants
LLM_API_STATUS_OK = "ok"
LLM_API_STATUS_ERROR = "error"
Expand Down Expand Up @@ -298,6 +317,28 @@ class ScanType(str, Enum):

ALL_PORTS = list(range(1, 65536))

# =====================================================================
# Geographic vantage-point comparison mode
# =====================================================================
# When comparison mode is enabled every selected node runs the SAME "comparison
# tier" of ports (so results can be compared across countries). The distribution
# choice controls what the tier is:
# - SLICE (default): the tier is the standard COMMON_PORTS bundle; the
# operator's chosen range is split across nodes for coverage (not compared).
# - MIRROR: the tier is the whole chosen range (plus COMMON_PORTS), so every
# port is compared across countries, at N x the work.

# Standard webapp/graybox feature bundle always run (mirrored to every node) in
# comparison mode so cross-country response divergence is meaningful even if the
# operator narrowed the selection. These are safe, unauthenticated checks; their
# methods are force-enabled (removed from excluded_features) when comparison
# mode is on. Referenced by feature id in FEATURE_CATALOG.
COMPARISON_GRAYBOX_BUNDLE_FEATURE_IDS = [
"web_discovery",
"web_hardening",
"web_api_exposure",
]

# =====================================================================
# Risk score computation
# =====================================================================
Expand Down
14 changes: 7 additions & 7 deletions extensions/business/cybersec/red_mesh/mixins/live_progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from ..graybox.models import GrayboxCredentialSet
from ..models import WorkerProgress
from ..constants import PHASE_ORDER, GRAYBOX_PHASE_ORDER
from ..connection_metrics import detect_connection_signals, merge_connection_windows

DEFAULT_PROGRESS_PUBLISH_INTERVAL = 30.0

Expand Down Expand Up @@ -175,7 +176,6 @@ def _status_rank(v):
all_phases[phase] = max(all_phases.get(phase, 0), dur)
if all_phases:
merged["phase_durations"] = all_phases
longest = max(metrics_list, key=lambda m: m.get("total_duration", 0))
# Merge stats distributions (response_times, port_scan_delays)
# Use weighted mean, global min/max, approximate p95/p99 from max of per-thread values
for stats_field in ("response_times", "port_scan_delays"):
Expand All @@ -193,12 +193,12 @@ def _status_rank(v):
"p99": round(max(s.get("p99", 0) for s in stats_list), 4),
"count": total_count,
}
# Success rate over time: take from the longest-running thread
if longest.get("success_rate_over_time"):
merged["success_rate_over_time"] = longest["success_rate_over_time"]
# Detection flags (any thread detecting = True)
merged["rate_limiting_detected"] = any(m.get("rate_limiting_detected") for m in metrics_list)
merged["blocking_detected"] = any(m.get("blocking_detected") for m in metrics_list)
# Merge aligned traffic evidence, then derive node signals from the combined
# sample counts. Legacy windows without counts remain visible but unverified.
connection_windows = merge_connection_windows(metrics_list)
if connection_windows:
merged["success_rate_over_time"] = connection_windows
merged.update(detect_connection_signals(connection_windows))
# Open port details: union, deduplicate by port
all_details = []
seen_ports = set()
Expand Down
Loading
Loading