Skip to content

MSP Friday ritual

Vendor-neutral multi-tenant workflow. No product branding in reports.

Command

licenselens batch tenants.yaml -o reports

Add --export action-plan|csv|json to write a structured activation backlog (§18) beside every tenant report — each tenant gets an action-plan.<ext> with its capability → current-evidence → implementation-category → reference → manual-validation metadata. This turns a portfolio-wide gap report directly into per-customer implementation work queues:

licenselens batch tenants.yaml -o reports --live --export json

With live credentials (default live auth mode is client_secret when --live is set):

export AZURE_CLIENT_ID=...
export AZURE_CLIENT_SECRET=...

licenselens batch tenants.yaml -o reports --live

Full CLI flags: CLI reference. Fixture and comments: examples/tenants.yaml.

App-only auth (once per customer)

  1. Entra ID → App registrations → New registration (single tenant or multi).
  2. Certificates & secrets → a client secret, a client certificate (PEM/PFX, used with auth: certificate), or a federated credential for OIDC.
  3. API permissions (application) + admin consent — see app-registration.md and permissions.md.
  4. Store tenant_id, client_id, and the secret in a secrets manager — not in git.

Prefer AZURE_CLIENT_ID / AZURE_CLIENT_SECRET (and per-tenant AZURE_TENANT_ID or YAML tenant_id) over embedding secrets in YAML. For secret-free unattended runs, prefer auth: certificate (cert path via AZURE_CLIENT_CERTIFICATE_PATH or a YAML certificate: key) or auth: oidc.

OIDC workload-identity federation (--auth oidc)

For scheduled, secret-free scans (for example GitHub Actions), LicenseLens supports OIDC workload-identity federation via --auth oidc. Instead of a stored client secret, an Azure ClientAssertionCredential is minted from a GitHub Actions OIDC token:

  • The token is fetched from the ACTIONS_ID_TOKEN_REQUEST_URL / ACTIONS_ID_TOKEN_REQUEST_TOKEN runtime environment variables that GitHub Actions injects into the job (or passed explicitly).
  • Only AZURE_TENANT_ID + AZURE_CLIENT_ID are required — no client secret is stored anywhere.
  • The Azure app registration must have a federated credential configured for the GitHub repository / environment / branch that runs the workflow.
export AZURE_TENANT_ID=...
export AZURE_CLIENT_ID=...
licenselens scan --auth oidc -o reports --report-archive

batch supports the same secret-free mode per tenant by setting auth: oidc (or auth_mode: oidc) in tenants.yaml. The same federated credentials / id-token: write setup applies; an explicit oidc_token may be passed per tenant, otherwise the GitHub Actions token is fetched from the runtime environment:

Scheduled continuous assessment (CI monitoring)

The repository ships a scheduled, secret-free monitoring workflow at .github/workflows/continuous-assessment.yml:

  • Schedule — runs daily at 30 7 * * * (07:30 UTC) and on demand via workflow_dispatch.
  • OIDC — runs licenselens scan --auth oidc -o reports --report-archive with id-token: write granted only here (least privilege; contents: read). Every action is pinned by full commit SHA (no floating tags).
  • Artifact upload — the machine-readable report JSON + ZIP are uploaded as an expiring artifact (retention-days: 14).
  • Step summary — a step emits the posture figure + top gaps from the JSON report to the GitHub step summary, so the run's summary shows the realized percentage and the top actionable gaps without printing secrets.

The workflow is guarded by licenselens.ci_guard.continuous_assessment_guards, which rejects unpinned actions, missing schedules, non-OIDC auth, and any client-secret / webhook secrets.

defaults + per-tenant merge

Top-level defaults is a map applied to every tenant. Each tenant entry is merged on top: non-null per-tenant keys win.

defaults:
  auth: client_secret
  # packs default to identity + endpoint when omitted

tenants:
  - slug: contoso
    tenant_id: "11111111-2222-3333-4444-555555555555"
  - slug: fabrikam
    tenant_id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
    workspace_resource_id: "/subscriptions/.../workspaces/..."

Tenant keys

Every key run_batch reads:

Key Notes
slug Report directory name (falls back to tenant_id)
tenant_id Entra tenant GUID
azure_tenant_id Alias for tenant_id
auth / auth_mode Values: dry-run, device, client-secret, azure-cli, oidc (underscore variants accepted; workload-identity/federated map to oidc)
oidc_token Optional explicit OIDC assertion for auth: oidc; when omitted, the GitHub Actions OIDC token is fetched from the standard environment
client_id App registration id (else AZURE_CLIENT_ID)
client_secret Accepted in YAML but must not be committed; prefer AZURE_CLIENT_SECRET
packs List or comma-separated pack ids. Default packs are identity + endpoint (not starter)
allow_email_proxy Opt into labeled Secure Score email path (email pack off by default)
profile Profile id
config Path to a custom profile/config YAML
rules Path to extra rules YAML
backend / backends Collector backends (string or list)
report_archive Write security-license-lens-report.zip for that tenant
workspace_resource_id Sentinel workspace ARM id
discover_workspaces Auto-select only when exactly one Sentinel workspace exists

Email is off by default (no Graph API for MDO policy config). Only set allow_email_proxy: true when the customer accepts a labeled Secure Score degraded path.

Secrets warning

client_secret in YAML is accepted by the code path, but real values must never be committed. Prefer environment variables and a secrets manager.

Output layout

Batch is the only command that nests reports:

reports/<slug>/<timestamp>/security-license-lens-report.{html,json,md}
reports/index.md

Single-tenant scan / demo / quickstart write flat into -o — see Report and export.

Failure behavior

One failing tenant is recorded in the summary rows and index.md; the batch continues with the remaining tenants. A single failure does not abort the batch.

Index sort

index.md lists tenants with exposed findings first (then by exposure count and realized %), so the Friday triage order is fix-exposed-first. Open each tenant HTML for the same top-card contract as a single scan.

Friday batch

export AZURE_CLIENT_ID=...
export AZURE_CLIENT_SECRET=...

licenselens batch tenants.yaml -o reports/$(date +%Y-%m-%d) --live
open reports/$(date +%Y-%m-%d)/index.md

Monthly diff

Point diff at two per-tenant JSON paths (note the slug/timestamp layout):

licenselens diff \
  reports/2026-07-01/contoso/<timestamp>/security-license-lens-report.json \
  reports/2026-08-01/contoso/<timestamp>/security-license-lens-report.json \
  -o reports/contoso-diff.md

Notes

  • Read-only. Advisory only. Confirm in the portal before change tickets.
  • Default packs for the top-card rollup are identity + endpoint. The starter pack exists but is not the batch default; pass it explicitly via packs or CLI --pack only when you want it.
  • JSON/ZIP reports contain tenant_id and evidence — treat as sensitive (report.md).