# Risk context for the JupyterLab workstation stack.
#
# 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.
#
# WHAT THIS MODELS: JupyterLab 4.6.0 delivered pre-installed and pre-configured
# as part of a data-science workstation stack. The customer runs what the
# vendor shipped and does NOT upgrade it independently — that is the work they
# bought the distribution to avoid. So there is no first-party application
# code, and reachability turns on the shipped CONFIGURATION and the deployment,
# not on a call graph through code we wrote.
#
# PROVENANCE: this is an approximation of a real vendor's product built from
# public information, not their bill of materials. Every configuration
# assumption would be recorded and reviewable in a real engagement; nothing
# here was tuned to make a verdict move.
schema_version: "1.0"

project:
  name: jupyterlab-stack
  summary: >
    JupyterLab 4.6.0 delivered pre-installed and configured on an enterprise data
    scientist's workstation, as part of a vendor-built, vendor-tested data-science
    software stack.
  open_source: true

reviewed:
  date: "2026-07-30"
  by: "Stephen Kuenzli (k9 Security)"
  commit: 1c56417

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: jupyterlab-runtime
    default: true
    applies_to: >
      JupyterLab as delivered and run by the analyst on their workstation: the
      Jupyter server process bound to loopback, the browser front end it serves,
      and every package installed alongside it in the delivered Python
      environment. This is the code that executes when the analyst opens a
      notebook, dataset or model artifact.
    paths:
      include:
        - /dist-stack/requirements.txt
        - /dist-stack/requirements-lock.txt
        - /dist-stack/jupyter_lab_config.py
        # JupyterLab's own workspace lock — listed in BOTH contexts on purpose.
        # It resolves the delivered front end (dompurify, vega, ajv, ...) AND
        # the vendor's build tooling (webpack, jest, typedoc), so binding it to
        # either context alone asserts something false. `paths` cannot separate
        # these — the split is carried by per-context reachability instead.
        - /yarn.lock
    frameworks:
      - jupyterlab
      - jupyter-server
      - tornado
      - notebook
      - nbdime
    asset_context:
      asset_criticality: tier_2
      network_exposure: isolated
      data_classification: confidential
      lifecycle: production

  - id: jupyterlab-build
    applies_to: >
      The tooling that BUILDS the JupyterLab front end — the yarn workspace
      monorepo's devDependencies (bundlers, test runners, linters, release
      tooling). This code runs in the vendor's build pipeline and is NOT
      delivered to a customer workstation. Per the context-follows-the-code-path
      rule it carries the build environment's context, not the delivered
      product's.
      ATTACKER MODEL (decides reachability here — do not re-derive it): the
      vendor's build consumes ONLY a pinned upstream JupyterLab release commit
      and registry packages resolved by the committed lockfile and verified by
      its integrity hashes. It runs no CI for third-party contributions,
      builds no customer- or user-supplied content, and fetches no unpinned
      artifacts. No untrusted actor supplies input to this pipeline; a
      compromised registry artifact would be a supply-chain event outside this
      file's reachability scope, not attacker-controlled input to a build tool.
    paths:
      include:
        # The yarn workspace lock is what an npm alert names — NOT package.json.
        # Shared with jupyterlab-runtime; see the note there for why, and why
        # this file does not pretend a glob can tell them apart.
        - /yarn.lock
        - /package.json
        - /builder/**
        - /buildutils/**
        - /testutils/**
    frameworks:
      - webpack
      - jest
      - yarn
    asset_context:
      asset_criticality: tier_2
      network_exposure: internal
      data_classification: internal
      lifecycle: dev

trust_model:
  untrusted_actors:
    - id: content-author
      description: >
        Whoever authored a notebook, dataset, image or model artifact that the
        analyst opens. Content arrives from colleagues, shared drives, public
        repositories and the hybrid cloud workflows the stack is sold for, so it
        is NOT authored by the person running it.
      # Anything listed here is ATTACKER-CONTROLLED INPUT during analysis.
      controls:
        - notebook cell source and cell output payloads
        - embedded markdown, HTML and SVG rendered by the front end
        - image files opened in the viewer
        - dataset and model-artifact file contents and file names
        - JSON settings files the front end reads, including overrides.json

    - id: extension-author
      description: >
        Whoever publishes a JupyterLab extension the analyst can discover and
        install. The delivered configuration leaves the Extension Manager at its
        default ("pypi"), which permits installation from PyPI, and sets no
        allowlist or blocklist.
      controls:
        - extension package name, metadata and installed code
        - plugin manifests and the lock rules the plugin manager enforces
        # Stated explicitly because it decides verdicts: schema CONTENT from an
        # installed extension is attacker-controlled input to the settings
        # registry's ajv validation pipeline (schema compilation, $id/$ref/URI
        # resolution) — the same rule as content-author's overrides.json above.
        # An analysis of a validation-pipeline dependency must not treat
        # settings schemas as vendor-only content.
        - settings schemas and defaults shipped with an installed extension,
          which the settings registry compiles and validates with ajv

  trusted_actors:
    - id: analyst
      description: >
        The data scientist, and the sole user of this JupyterLab instance. They
        are semi-trusted rather than adversarial.
      rationale: >
        The notebook kernel executes their arbitrary code BY DESIGN — it is a
        shell. So a vulnerability whose worst outcome is code execution in the
        analyst's own session grants them no capability they do not already
        hold, and on a single-user machine no privilege boundary is crossed.
        The analyst is the VECTOR by which untrusted content arrives, not the
        adversary. A finding is only attacker-controlled if one of the untrusted
        actors above supplies the input.

  out_of_scope_actors:
    - id: unauthenticated-network
      description: >
        The Jupyter server binds loopback (ServerApp.ip = 127.0.0.1) and is not
        reachable from the network. No remote party can address the port.

# Facts the AssetContext enum cannot express, recorded here because they change
# the analysis and would otherwise be silently absent.
#
# 1. NO ISOLATION. JupyterLab runs as the analyst's own user with nothing
#    separating it from that user's cloud credentials (~/.aws), SSH keys, source
#    repositories and authenticated browser sessions. A compromise of JupyterLab
#    is a compromise of the analyst's entire working environment. This is a
#    WEAKER containment posture than a production service, which would have a
#    scoped service account, a container boundary and tuned detection — so
#    "it is a desktop app" is a reason to look harder, not a reason to defer.
#
# 2. INBOUND AND OUTBOUND DIVERGE. `network_exposure: isolated` describes
#    INBOUND reach only. The workstation sits on the corporate network and
#    reaches cloud services for the hybrid workflows the stack is sold for, so
#    OUTBOUND reach is wide. An SSRF is therefore a pivot into the corporate
#    network from a host an attacker could not otherwise address, and must not
#    be argued down on the strength of the `isolated` value.
#
# 3. BLAST RADIUS IS PER-CUSTOMER, NOT PER-INSTANCE. One vulnerable delivered
#    build ships to every customer workstation. The `asset_criticality` value
#    describes a single instance; the vendor's exposure is the fleet.
#
# 4. THE CUSTOMER CANNOT PATCH. Remediation belongs to the vendor, who ships a
#    new stack release. A recommendation addressed to the end user to upgrade a
#    package is not actionable.

accepted_risks: []
