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
14 changes: 7 additions & 7 deletions src/detectmatelibrary/schemas/schemas.proto
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,21 @@ message ParserSchema {
optional string logID = 8;
optional string log = 9;
map<string, string> logFormatVariables = 10;
optional int32 receivedTimestamp = 11;
optional int32 parsedTimestamp = 12;
optional int64 receivedTimestamp = 11;
optional int64 parsedTimestamp = 12;
}

message DetectorSchema {
optional string __version__ = 1;
optional string detectorID = 2;
optional string detectorType = 3;
optional string alertID = 4;
optional int32 detectionTimestamp = 5;
optional int64 detectionTimestamp = 5;
repeated string logIDs = 6;
optional float score = 8;
repeated int32 extractedTimestamps = 9;
repeated int64 extractedTimestamps = 9;
optional string description = 10;
optional int32 receivedTimestamp = 11;
optional int64 receivedTimestamp = 11;
map<string, string> alertsObtain = 12;
}

Expand All @@ -46,9 +46,9 @@ message AggregateSchema {
repeated string detectorIDs = 2;
repeated string detectorTypes = 3;
repeated string alertIDs = 4;
optional int32 outputTimestamp = 5;
optional int64 outputTimestamp = 5;
repeated string logIDs = 6;
repeated int32 extractedTimestamps = 9;
repeated int64 extractedTimestamps = 9;
optional string description = 10;
map<string, string> alertsObtain = 12;
}
2 changes: 1 addition & 1 deletion src/detectmatelibrary/schemas/schemas_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 7 additions & 13 deletions src/detectmatelibrary/utils/time_format_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,21 +85,15 @@ def parse_timestamp(self, time_str: str, time_format: str | None = None) -> str:
if ts is not None:
return ts

# 3) Numeric epoch (seconds or milliseconds)
# 3) Numeric epoch (seconds, milliseconds, microseconds or nanoseconds)
if re.fullmatch(r"\d+(?:\.\d+)?", time_str):
try:
if "." in time_str:
val = float(time_str)
# if value looks like milliseconds (very large), normalize
if val > 1e12:
val /= 1000.0
return str(int(val))
else:
ival = int(time_str)
# heuristic: length >= 13 -> treat as milliseconds
if len(time_str) >= 13:
ival //= 1000
return str(ival)
val = int(time_str.split(".", 1)[0])
# Fold sub-second units until the value is a plausible seconds epoch.
# 100_000_000_000 seconds is the year 5138, so anything above that is ms/us/ns.
while val > 100_000_000_000:
val //= 1000
return str(val)
except (ValueError, OverflowError):
pass

Expand Down
29 changes: 29 additions & 0 deletions tests/test_common/test_extract_timestamp.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ def test_various_time_formats(self) -> None:
("0", 0),
("1772812294", 1772812294),
("1772812294.5", 1772812294),
# Sub-second epochs: ms, us and ns all fold down to seconds
("1772812294000", 1772812294),
("1772812294000000", 1772812294),
("1772812294000000000", 1772812294),
("1772812294000.5", 1772812294),
# Apache/nginx format
("04/Mar/2026:14:18:00 +0000", EXPECTED_UTC),
("04/Mar/2026:14:18:00", EXPECTED_UTC),
Expand Down Expand Up @@ -50,3 +55,27 @@ def test_various_time_formats(self) -> None:
assert result == [expected], (
f"Format '{time_str}': expected [{expected}], got {result}"
)

def test_microsecond_epoch_fits_the_schema(self) -> None:
"""Issue #271: a microsecond epoch folded only once landed in
milliseconds and overflowed the int32 timestamp fields."""
schema = schemas.DetectorSchema({
"extractedTimestamps": _extract_timestamp(
schemas.ParserSchema({"logFormatVariables": {"Time": "1643114452000000"}})
),
})
assert str(schema) # rebuilds the protobuf -- used to raise ValueError
assert schema["extractedTimestamps"] == [1643114452]

def test_timestamp_fields_hold_more_than_int32(self) -> None:
"""Timestamps are int64, so they survive 2038 (and a stray ms
value)."""
for schema, field, value in [
(schemas.ParserSchema(), "receivedTimestamp", 2**31),
(schemas.ParserSchema(), "parsedTimestamp", 2**31),
(schemas.DetectorSchema(), "detectionTimestamp", 2**31),
(schemas.DetectorSchema(), "receivedTimestamp", 1643114452000),
(schemas.AggregateSchema(), "outputTimestamp", 2**31),
]:
schema[field] = value
assert getattr(schema.get_schema(), field) == value
Loading