Skip to content

Redesigning Rules class to support easy addition of new rule types #191

Description

@swetha1654

I was looking to add support for Sigma rules in the otlp interface and noticed that
it uses the cosl library, especially the Rules class for rules normalization to a uniform
format, juju topology injection, and validation. Right now, the Rules class is tightly coupled to
Prometheus and Loki and addition of Sigma rules is not straight-forward.

Analysis of the rule format differences

Before I discuss how to restructure the Rules class, here are the key structural differences
between Prometheus / Loki and Sigma rule formats:

Aspect Prometheus / Loki Sigma
Grouping Rules must live inside groups Each rule is independent, no concept of grouping
Identify Group name + alert / record name UUID
Detection logic expr field that has PromQL/ LogQL queries detection block with selection and condition
Data source Implicit (the scrape target) logsource field (product / category / field)
Validation Valdiated using cos-tool Validated using pySigma library
Classification of Rules Rules can be either RecordingRule or AlertingRule There is only one format

Analysis of the current state of Rules class

The Rules class has the following public interfaces:

  1. add: Add rules from dict to the existing ruleset.
  2. add_path: Add rules from a directory.
  3. inject_and_validate_rules: Inject Juju topology labels and validate rules using CosTool.
  4. as_dict: Return standard rules file in dict representation (used to dump rules into the databag).

Internally, the class does the following:

  1. Ingest rules from multiple sources (either add_path / add)
  2. Normalize the rules to the "official" format (the rules must have the groups key)
  3. Injects Juju topology in:
    1. The group name (Prepends topology identifier + relative path to the group name, appends _rules suffix)
    2. Labels
    3. Expressions (modifies expr to contain the juju topology)
  4. Validates the syntax using CosTool.

Questions

  1. I noticed that the Juju topology injection happens both when add/add_path and
    inject_and_validate_rules is called. The former is called on the requirer side while the latter is
    used by the provider by injecting the same Juju Topology it receives from the requirer via databag.
    Is there a need to do it both times? It seems a bit redundant to me.
  2. Do you see any value in combining add and add_path into a single interface or keep it seperate?

Proposal

I propose the following new structure for Rules - a generic Rules and RuleBackend class.

Image

Rules class

  • add(rule_dict, source=None): Parses a rule dict via backend.from_dict (with the instance's
    topology) and appends the results to the internal _items: List[T].
  • add_path(dir_path, *, recursive=False): Reads rule files from a directory (or single file),
    builds a RuleSourceInfo from the user, then delegates each file to backend.from_dict.
  • as_dict(): Returns the accumulated items in the backend's output format by calling
    backend.as_dict(self._items).
  • inject_and_validate(rules, metadata): Reconstructs topology from the metadata dict, runs
    backend.from_dict for injection, then backend.validate for checking. Returns an InjectResult.

RuleBackend class

  • file_suffixes (property): Returns the file extensions this backend handles (e.g.
    [".rule", ".yml"] for Prometheus, [".yml", ".yaml"] for Sigma). Used by Rules._from_dir to filter files.
  • from_dict(rule_dict, *, source, topology): Takes a raw YAML-loaded dict, normalizes it into
    the backend's internal type T, and injects Juju topology. Returns List[T].
  • validate(rules): Takes the serialized output and checks it for correctness. Prometheus delegates to cos-tool; Sigma checks for required fields.
  • as_dict(items): Converts List[T] into the backend's output format. Prometheus produces {"groups": [...]}, Sigma produces {"rules": [...]}.

Pros and Cons

Pros:

  • Extensibility: Adding a new rule format means writing a new RuleBackend subclass.
  • Type safety: Rules[T] ensures that what from_dict produces is what as_dict consumes.

Cons:

  • This design also injects topology on both add methods as well as the inject_and_validate
    method. But this can just as easily be removed if we don't see any use case to have it both sides.
  • What i don't like is the RuleSourceInfo class leaking into the Sigma backend class as well.
    Currently the public method add exposes group_name and group_name_prefix which are customizable values
    specific to only the Prometheus/Loki backend. Groups are not a concept in Sigma rules. The RuleBackend template
    forces the from_dict method to consume it. Even though the SigmaRuleBackend doesn't have to use it in the method, it will remain in the function definition.

Happy to discuss this further and implement it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions