Skip to content

Commit 357bdfb

Browse files
RemindDedonyzpc
authored andcommitted
fix(sec-core): fix telemetry error mapping and non-finite float sanitization
1 parent 1333971 commit 357bdfb

3 files changed

Lines changed: 87 additions & 36 deletions

File tree

src/agent-sec-core/agent-sec-cli/src/agent_sec_cli/telemetry/sanitizer.py

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Map SecurityEvent details into telemetry business fields."""
22

3-
import copy
43
import json
4+
import math
55
from datetime import datetime, timezone
66
from typing import Any
77

@@ -12,9 +12,8 @@ def now_iso() -> str:
1212

1313

1414
def to_json_safe(value: Any) -> Any:
15-
"""Return a deep-copied JSON-safe representation of *value*."""
16-
copied = copy.deepcopy(value)
17-
return _make_json_safe(copied)
15+
"""Return a JSON-safe representation of *value*."""
16+
return _make_json_safe(value)
1817

1918

2019
def details_dict(value: Any) -> dict[str, Any]:
@@ -46,28 +45,18 @@ def request_value(details: dict[str, Any]) -> Any:
4645
return to_json_safe(details.get("request"))
4746

4847

49-
def error_value(details: dict[str, Any], result: dict[str, Any]) -> Any:
50-
"""Return the best available error value from event details/result data."""
51-
if "error" in details:
52-
return to_json_safe(details.get("error"))
53-
if "error" in result:
54-
return to_json_safe(result.get("error"))
55-
summary = result.get("summary")
56-
if isinstance(summary, dict) and "error" in summary:
57-
return to_json_safe(summary.get("error"))
58-
return None
48+
def error_value(details: dict[str, Any]) -> Any:
49+
"""Return the explicit error value from event details."""
50+
if "error" not in details:
51+
return None
52+
return to_json_safe(details.get("error"))
5953

6054

61-
def error_type_value(details: dict[str, Any], result: dict[str, Any]) -> Any:
62-
"""Return the best available error type from event details/result data."""
63-
if "error_type" in details:
64-
return to_json_safe(details.get("error_type"))
65-
if "error_type" in result:
66-
return to_json_safe(result.get("error_type"))
67-
summary = result.get("summary")
68-
if isinstance(summary, dict) and "error_type" in summary:
69-
return to_json_safe(summary.get("error_type"))
70-
return None
55+
def error_type_value(details: dict[str, Any]) -> Any:
56+
"""Return the explicit error type from event details."""
57+
if "error_type" not in details:
58+
return None
59+
return to_json_safe(details.get("error_type"))
7160

7261

7362
def result_value(result: dict[str, Any], key: str) -> Any:
@@ -77,10 +66,22 @@ def result_value(result: dict[str, Any], key: str) -> Any:
7766
return to_json_safe(result.get(key))
7867

7968

69+
def _is_json_scalar(value: Any) -> bool:
70+
"""Return whether *value* can be represented as a JSON scalar."""
71+
return value is None or isinstance(value, (str, bool, int, float))
72+
73+
74+
def _normalize_json_scalar(value: Any) -> Any:
75+
"""Return the strict JSON representation of a scalar value."""
76+
if isinstance(value, float) and not math.isfinite(value):
77+
return None
78+
return value
79+
80+
8081
def _make_json_safe(value: Any) -> Any:
8182
"""Convert arbitrary Python values into JSON-serializable values."""
82-
if value is None or isinstance(value, (str, int, float, bool)):
83-
return value
83+
if _is_json_scalar(value):
84+
return _normalize_json_scalar(value)
8485
if isinstance(value, dict):
8586
return {str(key): _make_json_safe(item) for key, item in value.items()}
8687
if isinstance(value, (list, tuple)):
@@ -93,7 +94,7 @@ def _make_json_safe(value: Any) -> Any:
9394
return _make_json_safe(model_dump())
9495

9596
try:
96-
json.dumps(value)
97-
except TypeError:
97+
json.dumps(value, allow_nan=False)
98+
except (TypeError, ValueError):
9899
return str(value)
99100
return value

src/agent-sec-core/agent-sec-cli/src/agent_sec_cli/telemetry/schema.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ def _build_seccore_record(event: SecurityEvent) -> dict[str, Any]:
4545
"seccore.call_id": value_or_none(event.call_id),
4646
"seccore.tool_call_id": value_or_none(event.tool_call_id),
4747
"seccore.request": request_value(details),
48-
"seccore.error": error_value(details, result),
49-
"seccore.error_type": error_type_value(details, result),
48+
"seccore.error": error_value(details),
49+
"seccore.error_type": error_type_value(details),
5050
"seccore.verdict": result_value(result, "verdict"),
5151
"seccore.summary": result_value(result, "summary"),
5252
"seccore.elapsed_ms": result_value(result, "elapsed_ms"),
@@ -70,8 +70,8 @@ def _build_baseline_record(event: SecurityEvent) -> dict[str, Any]:
7070
"baseline.result": value_or_none(event.result),
7171
"baseline.timestamp": _timestamp(event),
7272
"baseline.request": request_value(details),
73-
"baseline.error": error_value(details, result),
74-
"baseline.error_type": error_type_value(details, result),
73+
"baseline.error": error_value(details),
74+
"baseline.error_type": error_type_value(details),
7575
"baseline.passed": result_value(result, "passed"),
7676
"baseline.fixed": result_value(result, "fixed"),
7777
"baseline.failed": result_value(result, "failed"),

src/agent-sec-core/tests/unit-test/telemetry/test_schema.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def test_error_fields_map_from_exception_details() -> None:
189189
assert record["seccore.request"] == {"skill": "/path"}
190190

191191

192-
def test_error_fields_map_from_result_summary_when_present() -> None:
192+
def test_error_fields_do_not_fallback_to_result_summary() -> None:
193193
event = _event(
194194
event_type="pii_scan",
195195
category="pii_scan",
@@ -204,8 +204,34 @@ def test_error_fields_map_from_result_summary_when_present() -> None:
204204

205205
record = build_telemetry_security_event(event)
206206

207-
assert record["seccore.error"] == "bad input"
208-
assert record["seccore.error_type"] == "TypeError"
207+
assert record["seccore.error"] is None
208+
assert record["seccore.error_type"] is None
209+
assert record["seccore.summary"] == {
210+
"error": "bad input",
211+
"error_type": "TypeError",
212+
}
213+
214+
215+
def test_error_fields_preserve_explicit_details_null_over_result_values() -> None:
216+
event = _event(
217+
event_type="pii_scan",
218+
category="pii_scan",
219+
result="failed",
220+
details={
221+
"error": None,
222+
"error_type": None,
223+
"result": {
224+
"error": "ignored",
225+
"error_type": "IgnoredError",
226+
"summary": {"error": "bad input", "error_type": "TypeError"},
227+
},
228+
},
229+
)
230+
231+
record = build_telemetry_security_event(event)
232+
233+
assert record["seccore.error"] is None
234+
assert record["seccore.error_type"] is None
209235

210236

211237
def test_missing_fields_use_null_except_generated_event_id_timestamp_and_details() -> (
@@ -228,7 +254,7 @@ def test_missing_fields_use_null_except_generated_event_id_timestamp_and_details
228254
assert record["seccore.details"] == {}
229255

230256

231-
def test_mapping_deep_copies_and_converts_values_to_json_safe() -> None:
257+
def test_mapping_does_not_mutate_input_and_converts_values_to_json_safe() -> None:
232258
details = {
233259
"request": {"items": ("a", "b")},
234260
"result": {"summary": {"values": {"z", "a"}}},
@@ -242,3 +268,27 @@ def test_mapping_deep_copies_and_converts_values_to_json_safe() -> None:
242268
assert record["seccore.request"] == {"items": ["a", "b"]}
243269
assert record["seccore.summary"] == {"values": ["a", "z"]}
244270
json.dumps(record)
271+
272+
273+
def test_mapping_converts_non_finite_floats_to_null_for_strict_json() -> None:
274+
event = _event(
275+
details={
276+
"request": {
277+
"nan": float("nan"),
278+
"values": [float("inf"), float("-inf"), 1.25],
279+
},
280+
"error": float("nan"),
281+
"result": {
282+
"elapsed_ms": float("inf"),
283+
"summary": {"score": float("nan")},
284+
},
285+
}
286+
)
287+
288+
record = build_telemetry_security_event(event)
289+
290+
assert record["seccore.request"] == {"nan": None, "values": [None, None, 1.25]}
291+
assert record["seccore.error"] is None
292+
assert record["seccore.elapsed_ms"] is None
293+
assert record["seccore.summary"] == {"score": None}
294+
json.dumps(record, allow_nan=False)

0 commit comments

Comments
 (0)