MSP Friday ritual¶
Vendor-neutral multi-tenant workflow. No product branding in reports.
Command¶
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:
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)¶
- Entra ID → App registrations → New registration (single tenant or multi).
- Certificates & secrets → a client secret, a client certificate (PEM/PFX,
used with
auth: certificate), or a federated credential for OIDC. - API permissions (application) + admin consent — see app-registration.md and permissions.md.
- 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_TOKENruntime environment variables that GitHub Actions injects into the job (or passed explicitly). - Only
AZURE_TENANT_ID+AZURE_CLIENT_IDare 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 viaworkflow_dispatch. - OIDC — runs
licenselens scan --auth oidc -o reports --report-archivewithid-token: writegranted 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:
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
starterpack exists but is not the batch default; pass it explicitly viapacksor CLI--packonly when you want it. - JSON/ZIP reports contain
tenant_idand evidence — treat as sensitive (report.md).