# Risk context for NetBox v4.6.5.
#
# This is the file the customer writes. k9 never sees your source code; your
# agent analyzes reachability against it, and this file tells the agent where
# your code actually runs and who can reach it.
#
# Both execution contexts here are taken from NetBox's OWN packaging, not
# invented: the project ships two systemd units.
#
#   contrib/netbox.service      ExecStart=... gunicorn ... netbox.wsgi
#   contrib/netbox-rq.service   ExecStart=... manage.py rqworker high default low
#
# They share one virtualenv and one requirements.txt, so a dependency alert
# names a single manifest while its code runs in two processes with different
# network position. That is why one alert can carry two verdicts.
schema_version: "1.0"

project:
  name: netbox
  summary: >
    Self-hosted NetBox IPAM/DCIM at v4.6.5, serving a mid-size company's network
    documentation and automation. Web UI and REST/GraphQL API for authenticated
    internal users, plus a background worker for queued jobs.
  open_source: true

reviewed:
  date: "2026-07-30"
  by: "claude-opus-5, from NetBox's contrib/*.service units"
  commit: ebee3578

execution_contexts:
  # `default: true` so a dependency matching no context still scores. Adoption
  # of this file is never a precondition for getting a verdict.
  - id: netbox-web
    default: true
    applies_to: >
      The gunicorn WSGI service (contrib/netbox.service) serving the UI and the
      REST/GraphQL API. Handles every authenticated user request, including the
      OIDC SSO login flow.
    paths:
      include:
        - /requirements.txt
        - /base_requirements.txt
        - /netbox/**
    frameworks:
      - django
      - gunicorn
      - social-auth-core
    asset_context:
      asset_criticality: tier_2
      network_exposure: internal
      lifecycle: production
    deployment: >
      Reachable only from the corporate network, behind nginx. All access is
      authenticated via enterprise SSO (OIDC through python-social-auth); local
      accounts are break-glass only. Holds network documentation — infrastructure
      inventory and IP plans — which is sensitive internally but not regulated.

  - id: netbox-worker
    applies_to: >
      The RQ worker (contrib/netbox-rq.service, `manage.py rqworker high default
      low`) that executes queued jobs: webhook delivery, report and script runs,
      and scheduled housekeeping.
    paths:
      include:
        - /requirements.txt
        - /base_requirements.txt
        - /netbox/**
    frameworks:
      - django
      - rq
    asset_context:
      asset_criticality: tier_2
      # ISOLATED, not internal: the worker process has NO inbound listener. It is
      # driven only by jobs its own web tier enqueues through Redis. Same trust
      # zone and same data as the web context, so criticality is unchanged.
      network_exposure: isolated
      lifecycle: production
    deployment: >
      No listening socket; consumes from Redis on localhost. Runs with the same
      database credentials and the same code as the web tier, so a compromise
      here has comparable impact — the difference is network position, not blast
      radius. It performs the OUTBOUND HTTP for webhook delivery.


      WEBHOOK DESTINATIONS ARE STATIC HOSTNAMES. Every configured webhook's
      `payload_url` is a fixed internal endpoint; none templates a hostname from
      object data. NetBox renders `payload_url` as a Jinja2 template against the
      event context (extras/models/models.py:301-305, context built at
      extras/webhooks.py:52-58), so a template such as
      `https://{{ data.name }}.example.com/` WOULD put user-editable data in the
      host — this deployment does not do that. Templating in the path or body is
      unrestricted and unaffected by this statement.


      Stated because it decides a verdict and the code cannot: without it, the
      only user-influenced outbound path in this process is undecidable from
      source alone. Every other worker outbound call is settings-driven
      (core/jobs.py:102 CENSUS_URL, :209 RELEASE_CHECK_URL, core/plugins.py:133).

trust_model:
  untrusted_actors:
    - id: authenticated-internal-user
      description: >
        Any authenticated employee with a NetBox account reaching the web tier
        over the corporate network.
      controls:
        - Dashboard widget configuration, including the RSS widget's feed_url.
        # CONTENT, not destination. Kept explicit because the distinction decides
        # a verdict: object data is rendered into the webhook BODY, and into the
        # URL path where a template says so, but never into the HOSTNAME — see
        # the netbox-worker context's static-hostname statement.
        - Object data that becomes webhook payload content (request body and,
          where templated, the URL path — never the hostname).
  trusted_actors:
    - id: netbox-administrator
      description: Staff with admin rights over webhooks, data sources and scripts.
      rationale: >
        NetBox's own THREAT_MODEL.md designates script and template authoring as
        a trusted privilege — those permissions are equivalent to code execution
        by design, so abuse by a holder is not a vulnerability.
    - id: corporate-idp
      description: The enterprise OIDC provider issuing id_tokens.
      rationale: >
        id_tokens are retrieved server-to-server over the auth-code back channel,
        so their contents are trusted input rather than attacker-controlled.

# Empty is a good outcome. Nothing is accepted here: an acceptance suppresses a
# finding, and no NetBox finding in this set needs one. The trusted-privilege
# position on script authoring is recorded above as a trust-model fact, which is
# what it is, not as an accepted risk.
accepted_risks: []
