Skip to content
Open
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
22 changes: 21 additions & 1 deletion detectmatelibrary_tests/test_common/test_persist_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@


class TestPersistConfig:
def test_defaults(self):
def test_defaults(self, monkeypatch):
monkeypatch.delenv("STATE_DIRECTORY", raising=False)
cfg = PersistConfig()
assert cfg.path == "./state"
assert cfg.interval_seconds == 300
Expand All @@ -35,6 +36,25 @@ def test_extra_fields_rejected(self):
PersistConfig(unknown_field="value") # type: ignore


class TestPersistConfigStateDirectoryDefault:
def test_uses_state_directory_env_when_set(self, monkeypatch):
monkeypatch.setenv("STATE_DIRECTORY", "/var/lib/detectmate")
assert PersistConfig().path == "/var/lib/detectmate"

def test_takes_first_path_when_state_directory_is_colon_list(self, monkeypatch):
# systemd sets STATE_DIRECTORY colon-separated for multiple StateDirectory= entries
monkeypatch.setenv("STATE_DIRECTORY", "/var/lib/a:/var/lib/b")
assert PersistConfig().path == "/var/lib/a"

def test_falls_back_to_state_when_env_unset(self, monkeypatch):
monkeypatch.delenv("STATE_DIRECTORY", raising=False)
assert PersistConfig().path == "./state"

def test_explicit_path_overrides_state_directory_env(self, monkeypatch):
monkeypatch.setenv("STATE_DIRECTORY", "/var/lib/detectmate")
assert PersistConfig(path="s3://bucket/state").path == "s3://bucket/state"


class TestCoreDetectorConfigPersistField:
def test_persist_is_none_by_default(self):
cfg = CoreDetectorConfig()
Expand Down
18 changes: 17 additions & 1 deletion docs/detectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,27 @@ disables saving (backward compatible).
The detector name is automatically appended to `path`, so `path: ./state` for a detector
named `NewValueDetector` writes to `./state/NewValueDetector/`.

#### Running under systemd

The default `path` is CWD-relative. systemd services usually run with CWD `/`,
so `./state` would resolve to `/state` (wrong location, needs root). To avoid
this, set `StateDirectory=` in your unit file — systemd creates `/var/lib/<dir>`
with the right ownership and exports `$STATE_DIRECTORY`, which the default `path`
reads automatically. No explicit `path:` needed:

```ini
[Service]
User=detectmate
StateDirectory=detectmate # → state at /var/lib/detectmate/<detector>/
```

Setting `path:` explicitly (e.g. an `s3://` URL) always overrides `$STATE_DIRECTORY`.

#### Fields

| Field | Type | Default | Description |
|---|---|---|---|
| `path` | `str` | `"./state"` | Base directory or cloud URL. Detector name is appended. |
| `path` | `str` | `$STATE_DIRECTORY` or `"./state"` | Base directory or cloud URL. Detector name is appended. Defaults to systemd's `$STATE_DIRECTORY` if set, else `./state` (see note above). |
| `interval_seconds` | `int` | `300` | Background save interval in seconds. |
| `events_until_save` | `int \| null` | `null` | Save after this many ingested events. `null` disables event-count triggering. |
| `auto_load` | `bool` | `false` | Load saved state on construction. Raises `PersistencyLoadError` if no state exists. |
Expand Down
11 changes: 9 additions & 2 deletions src/detectmatelibrary/common/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from detectmatelibrary.utils import persistency
from detectmatelibrary.common.persist import init_persistency

from pydantic import BaseModel, ConfigDict
import os

from pydantic import BaseModel, ConfigDict, Field

from detectmatelibrary.schemas import ParserSchema, DetectorSchema

Expand All @@ -23,7 +25,12 @@
class PersistConfig(BaseModel):
model_config = ConfigDict(extra="forbid")

path: str = "./state"
# Default honors systemd's $STATE_DIRECTORY (set by StateDirectory= in the
# unit file) so services persist to /var/lib/<dir> with no explicit path=.
# Falls back to CWD-relative ./state outside systemd. Explicit path= wins.
path: str = Field(
default_factory=lambda: os.environ.get("STATE_DIRECTORY", "./state").split(":")[0]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
default_factory=lambda: os.environ.get("STATE_DIRECTORY", "./state").split(":")[0]
default_factory=lambda: (os.environ.get("STATE_DIRECTORY") or "./state").split(":")[0]

)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens when STATE_DIRECTORY="", or if it starts with a colon? overwrites the path with an empty string, then the complete path will become /MyDetector and also have permission errors (root)

interval_seconds: int = 300
events_until_save: int | None = None
auto_load: bool = False
Expand Down
Loading