Skip to content

feat(microsoft-defender-o365): mvp1/chk2 - define business configuration (pydantic base settings) (#493) - #494

Draft
Megafredo wants to merge 10 commits into
feat/471-chk1-project-architecture-setupfrom
feat/493-mvp1-chk2-define-business-configuration
Draft

feat(microsoft-defender-o365): mvp1/chk2 - define business configuration (pydantic base settings) (#493)#494
Megafredo wants to merge 10 commits into
feat/471-chk1-project-architecture-setupfrom
feat/493-mvp1-chk2-define-business-configuration

Conversation

@Megafredo

@Megafredo Megafredo commented Jul 16, 2026

Copy link
Copy Markdown
Member

Parent Issue: #493
Parent PR (MVP): #492

Contracts

Input Contract

None, this is a leaf configuration model with no upstream data dependency.

Output Contract

class DefenderO365Config(ConfigBaseSettings):
    tenant_id: str
    client_id: str
    use_certificate_auth: bool = False
    client_secret: Optional[str] = None
    client_cert_path: Optional[str] = None
    client_cert_thumbprint: Optional[str] = None
    base_url: str = "https://graph.microsoft.com/v1.0"
    filter_service_source: str = "microsoftDefenderForOffice365"
    rate_limit_requests_per_minute: int = 150
    max_fetch_retries: int = 5

Consumed by: CHK.3 (ConfigLoader wiring), CHK.4 (auth), CHK.6 (fetcher).

Acceptance Criteria

@configuration @validation @pydantic

Feature: Valid configuration loads without error via Pydantic BaseSettings
  As a developer
  I want my component configuration to validate on instantiation
  So that misconfigured deployments are caught at startup, not at runtime

  Background:
    Given the pydantic-settings library is available
    And environment variables are the primary configuration source

  Scenario Outline: Valid configuration with all required fields loads successfully
    Given <ENV_PREFIX>_<FIELD_1> is set to a non-empty string
    And <ENV_PREFIX>_<FIELD_2> is set to a non-empty string
    And <ENV_PREFIX>_<FIELD_3> is set to a non-empty string
    When <CONFIG_CLASS> is instantiated
    Then no ValidationError is raised
    And config.<FIELD_1_ATTR> is not None
    And config.<DEFAULT_FIELD> equals "<default_value>"

    Examples:
      | ENV_PREFIX       | FIELD_1    | FIELD_2    | FIELD_3       | CONFIG_CLASS      | FIELD_1_ATTR | DEFAULT_FIELD | default_value |
      | <COMPONENT_ENV>  | <FIELD_A>  | <FIELD_B>  | <FIELD_C>     | <ConfigClass>     | <attr_a>     | <attr_d>      | <val_d>       |
@configuration @validation @fail-fast

Feature: Missing required field raises a ValidationError at instantiation
  As a developer
  I want the config model to fail immediately when a required field is absent
  So that I get a clear error message instead of a runtime NoneType crash

  Background:
    Given the pydantic-settings library is available
    And all fields except the tested one are correctly set

  Scenario Outline: Missing required field raises ValidationError referencing the field
    Given <ENV_PREFIX>_<REQUIRED_FIELD> is not set
    And all other required fields are present
    When <CONFIG_CLASS> is instantiated
    Then a ValidationError is raised
    And the error references the "<error_field>" field

    Examples:
      | ENV_PREFIX      | REQUIRED_FIELD | CONFIG_CLASS  | error_field      |
      | <COMPONENT_ENV> | <FIELD_NAME>   | <ConfigClass> | <field_name_lc>  |
@configuration @validation @constraint

Feature: Numeric field below minimum raises a ValidationError
  As a developer
  I want numeric configuration fields to enforce minimum bounds
  So that invalid operational parameters are rejected at startup

  Background:
    Given the pydantic-settings library is available
    And all required fields are set to valid values

  Scenario Outline: Numeric field below minimum threshold raises ValidationError
    Given all required fields are set
    And <ENV_PREFIX>_<NUMERIC_FIELD> is "<invalid_value>"
    When <CONFIG_CLASS> is instantiated
    Then a ValidationError is raised
    And the error references the "<error_field>" field

    Examples:
      | ENV_PREFIX      | NUMERIC_FIELD      | invalid_value | CONFIG_CLASS  | error_field        |
      | <COMPONENT_ENV> | <FIELD_NAME>       | 0             | <ConfigClass> | <field_name_lc>    |
      | <COMPONENT_ENV> | <FIELD_NAME>       | -1            | <ConfigClass> | <field_name_lc>    |
@configuration @validation @cross-field

Feature: Cross-field validator enforces mutually-exclusive field group consistency
  As a developer
  I want the config model to reject inconsistent field combinations
  So that incompatible authentication or operational modes are caught at startup

  Background:
    Given the pydantic-settings library is available
    And a @model_validator enforces group consistency

  Scenario Outline: Mode A enabled without Mode A required fields raises ValidationError
    Given <ENV_PREFIX>_<MODE_FLAG> is "<mode_flag_value>"
    And <ENV_PREFIX>_<MODE_A_FIELD_1> is not set
    And <ENV_PREFIX>_<MODE_A_FIELD_2> is not set
    When <CONFIG_CLASS> is instantiated
    Then a ValidationError is raised
    And the error references "<error_field_a>" or "<error_field_b>"

    Examples:
      | ENV_PREFIX      | MODE_FLAG        | mode_flag_value | MODE_A_FIELD_1 | MODE_A_FIELD_2 | CONFIG_CLASS  | error_field_a    | error_field_b      |
      | <COMPONENT_ENV> | <FLAG_NAME>      | true            | <FIELD_A>      | <FIELD_B>      | <ConfigClass> | <field_a_lc>     | <field_b_lc>       |

  Scenario Outline: Mode B enabled without Mode B required fields raises ValidationError
    Given <ENV_PREFIX>_<MODE_FLAG> is "<mode_flag_value>"
    And <ENV_PREFIX>_<MODE_B_FIELD> is not set
    When <CONFIG_CLASS> is instantiated
    Then a ValidationError is raised
    And the error references "<error_field>"

    Examples:
      | ENV_PREFIX      | MODE_FLAG   | mode_flag_value | MODE_B_FIELD | CONFIG_CLASS  | error_field    |
      | <COMPONENT_ENV> | <FLAG_NAME> | false           | <FIELD_C>    | <ConfigClass> | <field_c_lc>   |

Done Checklist

  • Create src/models/settings/source_configs.py with DefenderO365Config(ConfigBaseSettings)
  • Declare all fields with types, Field(...) descriptions, and defaults
  • Add @field_validator for tenant_id (non-empty) and rate_limit_requests_per_minute (>= 1)
  • Add @model_validator for auth mode consistency
  • Write unit tests: valid credential, valid cert, missing tenant, rate limit = 0, cert mode missing fields, credential mode missing secret
  • Update README.md with MICROSOFT_DEFENDER_O365_* env variable table
  • Create a dedicated PR linked to the Umbrella issue

Closes #493

@Megafredo Megafredo added the filigran team Item from the Filigran team. label Jul 16, 2026
@Megafredo
Megafredo changed the base branch from feat/471-chk1-project-architecture-setup to main July 16, 2026 09:53
@Megafredo
Megafredo changed the base branch from main to feat/471-chk1-project-architecture-setup July 16, 2026 09:55
@Megafredo
Megafredo force-pushed the feat/471-chk1-project-architecture-setup branch from 4a386b6 to 1d831ac Compare July 17, 2026 13:31
Megafredo and others added 5 commits July 17, 2026 16:19
…branch (#471)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… branch (#493)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…k2 config (#493)

- 1 HAPPY PATH feature: valid_configuration_loads (tests/features/)
- 3 UNHAPPY PATH features: missing_required_field, numeric_field_below_minimum,
  cross_field_auth_mode_consistency (tests/constraints/)
- PR #494 body provided a generic Gherkin template with placeholders; resolved them against
  the DefenderO365Config contract and Done Checklist (confirmed with user before writing)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Megafredo
Megafredo force-pushed the feat/493-mvp1-chk2-define-business-configuration branch from ee1400d to cb5580c Compare July 17, 2026 15:06
@Megafredo Megafredo self-assigned this Jul 20, 2026
Megafredo and others added 4 commits July 20, 2026 16:06
…ses (#493)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…onftest comment (#493)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…493)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…#493)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@mariot mariot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just a small request about the secret string

"source_use_certificate_auth": {
"data": self.source.use_certificate_auth
},
"source_client_secret": {"data": self.source.client_secret},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this passes a SecretStr object into daemon config, not the actual string secret. Downstream code typically expects a plain string (other collectors call .get_secret_value())

source_client_secret should likely unwrap SecretStr before forwarding (self.source.client_secret.get_secret_value() when set). Passing the SecretStr object directly can break downstream auth config handling

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point @mariot .
However, this is intentional. I'd prefer to keep the value as a SecretStr for as long as possible and only call get_secret_value() at the point where the secret is actually consumed. Unwrapping it during configuration serialization defeats much of the purpose of using SecretStr, since it turns the secret into a plain string earlier than necessary.

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

Labels

filigran team Item from the Filigran team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(microsoft-defender-o365): mvp1/chk2 - define business configuration (pydantic base settings)

4 participants