Metadata-Version: 2.5
Name: openhound-saml-producer
Version: 0.4.6rc1
Summary: Contract-conformant OpenGraph SAML node and edge builders for OpenHound extensions
Author-email: Jim Sykora <jsykora@specterops.io>
License-Expression: Apache-2.0
Requires-Python: <3.15,>=3.13
Requires-Dist: openhound>=0.4.1
Description-Content-Type: text/markdown

# openhound-saml-producer

This library builds the SAML graph nodes and edges for OpenHound extensions.
It is a prototype. It is published on PyPI as `openhound-saml-producer`.
See [Release steps](#release-steps).

Two extensions that see the same SAML endpoint write the same node. For
example, an issuer or an assertion consumer service (ACS) route gets the same
ID in every extension. The ACS is the URL where a service provider receives
SAML assertions. Each extension writes its own environment ID on that node as
`environmentid`. The correlator later decides the owner's environment.

## What it does

- Builds owned nodes for identity providers, service providers, and claim
  mappings. Their IDs come from the producer name, the environment, and the
  native key.
- Builds shared nodes for issuers and ACS routes. Their IDs come from the
  route strings only. Each shared-node builder takes the producer's environment
  ID, which it writes as `environmentid`.
- Builds account resolution rules and fields (v0.3). A producer gives typed
  conditions (`AccountResolutionCondition`). The library writes the rule text
  from the profile templates, so a producer never writes rule text.
- Rejects extra properties that do not start with the producer prefix.
- Builds v0.4 set grants (`SetPolicyFact`, `set_grant_edges`), claim
  projections, eligibility exceptions (`EligibilityExceptionFact`,
  `eligibility_exception_edge`), and claim value residuals (a `ClaimValueFact`
  with a `set_policy`). A fact carries v0.4.0 only when it uses a v0.4 kind or
  property. All other output stays v0.3.0.
- Builds the portable principal set (v0.4), only for a platform that has no group
  node in the graph. `PrincipalSetFact` gives a `SAML_PrincipalSet` node whose ID is
  the contract's portable set key (`principal_set_id`). `SetMembershipFact` gives one
  `SAML_EffectiveMemberOfPrincipalSet` edge. A `SetPolicyFact` with the profile
  `saml_principal_set_membership_v1` grants access from that node. A provider
  should use the portable profile only when the platform has no group node:
  `assert_conformant` returns a warning for a provider that uses both profiles.
- Declares the v0.4 edges: `edge_defs` takes `set_kinds` for set grants and
  exceptions, and `NodeKind.PRINCIPAL_SET` declares the membership edges.
- Writes assertion requirements. A `ServiceProviderFact` with
  `assertion_requirements` (registered profile IDs, for example
  `aws_iam_role_session_name_v1`) is a v0.4.1 fact with the property
  `assertion_requirement_profiles`. An unknown ID is a `FactError`.
- Writes the same relationship context on every edge.

## IdP example

This example writes one identity provider, its tenant issuer, and one route.

```python
from openhound_saml_producer import IssuerFact, Producer, RouteFact
from openhound_saml_producer.conventions import entra_external_route
from openhound_saml_producer.facts import FederationProviderFact
from openhound_saml_producer.idp import federation_provider_edges, federation_provider_node
from openhound_saml_producer.shared import acs_node, issuer_node

TENANT = "11111111-2222-3333-4444-555555555555"
PRODUCER = Producer(name="entra_saml", extension="openhound-entra-saml",
                    prefix="az_", technology="entra", source_kind="SAML")

provider = FederationProviderFact(
    environment_id=f"entra-saml-tenant:{TENANT}",
    native_key="22222222-3333-4444-5555-666666666666",
    native_kind="AZServicePrincipal",
    native_object_id="",  # empty: the native key is shown
    name="Example app",
    enabled=True,
    extras={"az_tenant_id": TENANT},
)
issuer = IssuerFact(entity_id=f"https://sts.windows.net/{TENANT}/")
route: RouteFact = entra_external_route(TENANT)

nodes = [
    federation_provider_node(PRODUCER, provider),
    issuer_node(PRODUCER, provider.environment_id, issuer),
    acs_node(PRODUCER, provider.environment_id, route),
]
edges, warnings = federation_provider_edges(
    PRODUCER, provider, native=None, issuer=issuer,
    routes=[route], claim_mapping_keys=[],
)
```

`warnings` holds `BuildWarning` values. The extension logs them. They are not
graph output.

## SP example

This example writes one service provider and its trust of the same issuer.

```python
from openhound_saml_producer.facts import ServiceProviderFact
from openhound_saml_producer.sp import service_provider_edges, service_provider_node

sp = ServiceProviderFact(
    environment_id=f"entra-saml-tenant:{TENANT}",
    native_key="sp-native-key",
    native_kind="ExternalFederation",
    native_object_id="",
    name="Example trust",
    enabled=True,
)
sp_node = service_provider_node(PRODUCER, sp)
sp_edges, sp_warnings = service_provider_edges(
    PRODUCER, sp, native=None, trusted_issuer=issuer, routes=[route],
)
```

The edge ends for `issuer` and `route` use the same IDs as the shared nodes
above. You do not compute those IDs yourself.

## Limits

- Node and edge IDs use the rules in the shared normalization design. Do not
  change them without a contract review.
- The library writes no timestamp on nodes. BloodHound adds its own.
- The checks in `openhound_saml_producer.testing.conformance` run on whole
  payloads. `assert_conformant` checks one payload. `assert_shared_nodes_agree`
  checks that two producers' copies of one shared node match, except for
  `environmentid`. Use them in tests.
  `assert_conformant` also checks the contract and relationship-context
  versions on every edge, rejects unprefixed extra edge properties, requires
  each `SAML_*` node to carry exactly its own kind and `SAML`, and applies the
  correlator's account-evidence rules to `SAML_EligibleFor` and `SAML_HasAccount`
  edges. Each fact must carry v0.4.1 if it lists assertion requirements, v0.4.0 if
  it has other v0.4 content, and v0.3.0 if it has none. The v0.4 rules agree with the correlator. A passing check returns
  a list of warnings. One warning is for a provider that has a complete set
  grant and no expanded facts: stop expanded facts only after the rollout gate
  (design section 14.5) passes.

## Release steps

A GitHub Release publishes the package to PyPI. Publication has these steps.

1. Set `version` in `pyproject.toml`, for example `0.4.6rc1`. Merge the change
   to `dev-jim` for a release candidate, or to `main` for a stable version.
2. In GitHub, create a Release. Set the tag to `saml-producer-v<version>`, for
   example `saml-producer-v0.4.6rc1`.
3. Set the target branch. Use `dev-jim` for a release candidate and `main` for a
   stable version.
4. Set the prerelease flag. Set it for a release candidate (`a`, `b`, or `rc`
   version). Clear it for a stable version.
5. Publish the Release. The workflow `release-saml-producer.yml` builds the
   files. It checks the version and the branch. It stops if a check fails.
6. Approve the `pypi` environment deployment in GitHub Actions. PyPI gets the
   files only after this approval.
