Repository navigation
v0.6.0-beta
Pre-releaseBreaking
- Python 3.11 is now the minimum version. Python 3.10 reached end of life
in October 2026 and gets no more security fixes.requires-pythonis now
>=3.11, sopip installrefuses a 3.10 interpreter.paramify doctoralso
reports 3.10 as too old. To upgrade, make a new virtual environment on Python
3.11 or later and install again. The ruff and mypy targets move to 3.11 too. paramify issues uploadsends reports into Paramify pipelines. The API
key needs new permissions, and each issue-report entry must now set
close_cycle(next entry). The command no longer calls
POST /assessment/{id}/intake. Per assessment it
uploads each report bare toPOST /pipelines/{id}/intake, then queues one
POST /pipelines/{id}/processnaming exactly those artifacts, then waits for
the job and prints its counts (created, updated, seen-closed, auto-closed). The
call isPROCESS_CLOSEonly when the assessment'sclose_cycleisafter_run
and every target bound to it in the run succeeded and uploaded; otherwise
PROCESS, with the reason the close was skipped. The pipeline puts an upload
on its oldest open cycle, soeffectiveDateno longer routes it. The API key
needsPIPELINE_INTAKE, plusPIPELINE_PROCESS, plusPIPELINE_CLOSEto
close. A job that fails, is blocked behind a failed job, or outlasts
--wait-timeout(default 900 s) fails the command;--no-waitqueues and
exits. The intake log now also records each job, so a re-run processes files
an interrupted run uploaded but never processed, and waits on a job it left
running. The TUI's issue-report panel shows the planned operation per
assessment and warns in the confirm when a run will close a cycle.- Every issue-report entry must set
close_cycle. It is a reserved config
key,after_runornever, with no default. Until an entry sets it, the
uploader holds back that entry's reports. Closing a cycle auto-closes every
open issue the cycle never saw, so it states how the customer fills the
assessment's cycles: one report per cycle closes after each complete run;
several files per cycle close only when the last is in.paramify assessments selectasks for it (or takes--close-cycle), and so does the TUI's
assessment picker.paramify validatereports an entry without one, reading
entry andplatforms.<category>config the way the runner does. - A manifest path or name that does not exist is now an error. Every
command resolves a manifest name the same way: as typed, then under
manifests/, then with.yaml. A name that matches nothing exits 1. Before,
some commands read it as an empty manifest,-f democould create./demo,
anddoctorpassed.paramify validate demonow works. - A malformed
${env:...}reference is now an error. A reference must name
an upper-case variable, such as${env:OKTA_API_TOKEN}. A reference that does
not match, such as a lower-case name, now fails the schema check and secret
resolution. Before, the runner passed it through as the literal secret value. - A target's
profileoutranks static AWS keys in the environment. When a
target setsprofile, the runner removesAWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEYandAWS_SESSION_TOKENfor that invocation and logs a
note. Before, keys in the environment silently won over the profile. A profile
that usescredential_source = Environmentno longer works, because those
keys are gone when it runs. Categories can declareauth.selector_envand
auth.credential_envto describe the same rule for other clouds. - The Okta account allowlists are configuration, not code.
okta_iam_core.pycarried a hardcoded list of one service account and two named
people's email addresses, used to seed service-account and Super Admin detection
for a single tenant. Both lists now come from config and default to empty:
OKTA_KNOWN_SERVICE_ACCOUNTSonokta/non_user_accounts_authenticationand
OKTA_KNOWN_SUPER_ADMINSonokta/least_privilege, each declared in that
fetcher'sconfig_schemaand accepting a comma- or newline-separated list of
logins/emails. No behaviour change for a tenant that sets them; every other
tenant now gets only the name-independent detection methods (userType, API token
ownership, OAuth client assignment, group membership, assigned admin roles),
which is what it should always have been. write_statusis gone fromazure_commonandgcp_common. It was an
alias ofreport_failure, kept for older fetchers. A custom fetcher that
imports it must importreport_failureinstead.
Security
- Jira fetchers (
jira_site_inventory,jira_remediation_items,
jira_audit_records,jira_access_roles):JIRA_BASE_URLmust now be a Jira
Cloud site (https://<site>.atlassian.net, or the Government Cloud domain) or
the API gateway for a scoped token (https://api.atlassian.com/ex/jira/<cloudId>).
Any other host, a userinfo part, a query, a fragment or a port is refused before
the token is sent, and the error no longer echoes the value. HTTP redirects are
no longer followed; a 3xx answer is abad_configfailure. The README documents
a read-only scoped token for the admin collector. jira_remediation_items,jira_audit_records,jira_access_roles: hitting a
collection cap now fails the run withpartial_failure, and every evidence file
carriesmetadata.completeandmetadata.incomplete_reason.
jira_audit_recordsno longer reportsrecords_truncated: falsewhen records
past the cap were dropped.jira_change_requests,jira_incident_postmortems: a configured field name
that matches no Jira field, or several, now fails the run aspartial_failure
instead of silently skipping the checks that need it. Ambiguous names list their
matching ids inmetadata.field_candidates.jira_change_requests,jira_incident_postmortems(review of PR #174): a
check whose field role is turned off no longer produces its finding on every
ticket;metadata.checks_disabledlists the findings that cannot be produced,
and a setting that depends on a turned-off field (required severities, required
phases, category targets, report window, or no approval source at all) is
refused asbad_config. A failedissue/bulkfetchmarks its keys as not
visible (postmortem_not_visible,action_items_not_visible) instead of
reporting the post-mortem or action items as absent. The deployment time of a
reopened change is its first move into the done status, so a late approval is
still caught. A role may be pinned to severalcustomfield_ids for
team-managed projects; changelog reads with more than 10 field ids are made in
groups instead of being cut to 10. Pinned ids are checked against the site's
field list, a value that is neither a name, an id nor a list of ids is
bad_config, the deployment time of a change is its first entry into any
done-category status (read fromGET /rest/api/3/status), and turning off
change_typewhile change types are pre-approved is refused.- All Jira fetchers:
metadata.partial_failurenow follows
metadata.complete(a hit cap or an unresolved field is partial, not only a
failed call). HTTP 429 and 5xx are retried withRetry-Afteror backoff, and a
request that still gets no answer after the first success is one failed call
rather than the end of the run.jira_site_inventory(0.1.1) uses the shared
client instead of its own drifted copy. jira_access_roles: reads only the project roles named in a privileged
grant (JIRA_ACCESS_ALL_ROLES=truereads every role), lists grants to holder
types it cannot resolve (reporter,assignee, custom fields) as
unresolved_privileged_grants, tolerates a product the site does not have in
thegroup/bulkfallback, and states that organization admins are not included.jira_audit_records: sensitive changes are matched by key words in the
category name withsummary.unclassified_categoriesfor what matched nothing
(Atlassian does not publish the category list); changed values are kept only
for sensitive records, shortened and markup-escaped; fewer records than the
server'stotalis reported as partial.jira_access_roles: reading no admin group at all is a recorded failure,
never an empty result.jira_site_inventory: a bad token isauth_failed
again (the credential is checked on/myselfbefore the anonymous
serverInfo), and hitting the issue cap now fails the run as partial like the
other Jira fetchers.- All Jira fetchers (second review): summary counts are
null, not0, for
a section whose fetch failed or did not finish, so an assessor never reads a
refused call as "none found". A date-only approval date is compared with the
deployment at day granularity (same day is unknown, never a finding), and a
date-only detected or reported time isreport_time_not_measurableinstead of
a falsereported_late.jira_remediation_itemsmeasures a done ticket with
noresolutiondatefrom its status history and counts
closed_without_close_time.jira_change_requestsclaims only KSI-CMT-LMC
(CNA-RNT and CNA-ULN dropped).search_issuesstops on a repeated page token. - All Jira fetchers (third round, from an adversarial review of the second):
a check that could not run because a read failed is listed per ticket under
unknown, never underfindings(approval_unknownwhen a status-based
approval's history read failed;phases_unknown,postmortem_not_visible,
action_items_not_visible; a remediation ticket'sclosed_at_source: unknown), andbulk_changelogsnow says which issues' history it failed to
read. A capped search, a stopped collection (an unexpected response shape)
and, injira_access_roles, a failed role, role-index, admin-group listing or
membership read null the summary section they belong to and no other.
summary.action_itemsis null when any post-mortem or action item was not
read. Changelog times are rendered in the ticket's own UTC offset before they
are compared with a date-only field (bulkfetch gives epoch milliseconds, which
is UTC), so a closure at 20:00 local on the due day is no longer "after the due
date".is_date_onlyrequires a parseable date, so10/05/2026is
unparseable, not unmeasurable.jira_remediation_items,jira_audit_records
andjira_site_inventorynow survive an unexpected response shape
(internal_errorwith the evidence written) like the other three, and
jira_site_inventorynulls its project or issue counts when that read failed
or the issue search hit its cap. History times follow the collector's IANA
timezone from/myself(DST included), not a fixed offset; a done
remediation ticket's close time is its last entry into the done category by
status id (GET /rest/api/3/status, sojira_remediation_itemsnow needs
read:status:jirawith a scoped token), withreopenedrecorded; summary
changes_approval_unknownnames deployed changes whose approval could not be
read;jira_access_rolesmarks a group whose membership read failed with
membership_complete: falseand null counts instead of "0 members". A done
remediation ticket whose close time cannot be taken from the history (an
unparseable time, or the status catalog unread) hasclosed_at_source: unknownrather than a guessed date; an open ticket closed once before is
reopened. jira_site_inventoryscopes documented: the README scope table had no row
for it. Found on the first scoped-token run:GET /rest/api/3/priority/search
needsread:priority:jira, which the other Jira fetchers do not use.jira_site_inventorybehaviour change (from the base URL allowlist above):
JIRA_BASE_URLnow acceptshttps://<site>.atlassian.net,
https://<site>.jira.com, the Government Cloud domain and the scoped-token
gateway, and nothing else. A URL with a path, query, port or userinfo, which the
old client passed through torequests, is now refused asbad_config.
Added
-
CI runs the tests on Python 3.15. The test matrix is now 3.11 to 3.15.
The 3.10 row is gone. -
azure_devops_audit_streams(EVD-AZURE-DEVOPS-AUDIT-STREAMS): whether an
Azure DevOps organization exports its audit log, which it otherwise keeps for
90 days. Records:- whether auditing is on, from the Log Audit Events policy when the token can
read it, otherwise from the audit log: its newest change to that policy, else
its newest event (a scoped PAT gets 401 on the policy, and the run notes it;
signed in with Entra instead, the fetcher reads the policy itself); - every audit stream's target, status, reason and last update;
- the newest audit event, not counting the fetcher's own reads of the log.
For an Azure Monitor Logs stream it counts this organization's
AzureDevOpsAuditingrows in the workspace over the window, andmissing_rows
flags an enabled stream that delivered nothing while the organization was
active. The organization is matched on organization-level events and rows
only: project-level rows carry the project's id, so they are counted apart
rather than as other organizations, and a stream is only expected to have
delivered events newer than itself. Stream tokens and keys are never written. - whether auditing is on, from the Log Audit Events policy when the token can
-
azure_devops_pipelines(EVD-AZURE-DEVOPS-PIPELINES): the first Azure DevOps
fetcher, one target per organization, signed in with a PAT (AZURE_DEVOPS_PAT)
or, when none is set, the ambient Entra identity (az login, managed or workload
identity, or a service principal added to the organization). Each YAML
pipeline's final YAML comes from a preview run pinned to the default branch's
head commit, so templates are expanded and nothing is queued. If the preview is
refused, the file is read at that commit instead.yaml_commitand
yaml_sha256let an assessor fetch the same YAML and re-check it. Steps are
sorted into test, security scan, IaC validation, infrastructure and application
deploy, signing, signature verification and SBOM.tests_gate_deploysreports
whether a failing test would stop every deploy, followingcontinueOnError,
conditions such asalways()andsucceededOrFailed(), and stage and job
dependsOn;ungated_deploysnames the rest and why. The fetcher also collects:- completed runs and published test runs over the lookback; test runs are queried
in 7-day slices, the most the API allows, andnot_passed_testscomes from the
totals, so triaging failures doesn't change it; - each environment's approvals, branch control, required-template and
artifact-evaluation checks, as configured at collection time; - every deployment in the window, joined to its run's branch, commit and test
results; - each repository's default-branch policies.
requires_reviewdoesn't count the
author's own vote, andself_approval_satisfies_reviewflags policies the
author can satisfy alone; - the project's pipeline settings;
- a count of classic release pipelines and their deployments in the window.
summary.not_collectedlists what is out of scope: approval decisions, policy
bypass permissions, and the content of classic pipelines. Signing steps are
framed as supporting evidence for KSI-SVC-VRI. A failed call names the PAT scope
it needs. With a PAT, environments need Environment (Read & manage), because
Azure DevOps has no read-only scope for them; an Entra identity needs only read
access, such as project Readers. The shared client
(fetchers/azure/_shared/devops_rest.py) handles PAT or Entra sign-in
(metadata.auth_mode) and continuation paging, and records the identity it
collected as (metadata.collecting_identity, from_apis/connectionData) for
every Azure DevOps fetcher. It sends credentials only to the host it paged from
and never follows a redirect, and it keeps the TF/VS error code on every
failure. - completed runs and published test runs over the lookback; test runs are queried
-
A ServiceNow category with two fetchers.
servicenow_casescollects
compliance review cases, andservicenow_changescollects ITSM change records.
Both use a bearer token (SERVICENOW_API_TOKEN) and take an instance URL for
each target. An optional last-run watermark limits a pull to new records.
Leave it unset for the first pull. -
Four Azure Front Door fetchers.
azure_front_door_waf_policies,
azure_front_door_waf_coverage,azure_front_door_tlsand
azure_front_door_originsread Front Door Standard and Premium profiles.
Classic Front Door and classic CDN profiles are counted as skipped. All four
need only Reader access and run against each subscription. Theazureextra
addsazure-mgmt-frontdoorandazure-mgmt-cdn. -
azure_defender_assessmentscollects each Defender for Cloud assessment
for each resource, with its severity, status and resource id. Severity comes
from the assessment itself in Resource Graph, and falls back to the
subscription's metadata catalogue. Each record has aseverity_source, and
the summary counts the records whose severity came from the catalogue or is
unknown. A failed Resource Graph query is a collection failure (exit 1). A
subscription whereMicrosoft.Securityis not registered is recorded as
not_registered, not as a failure. -
paramify capabilities list|showandparamify artifacts list|pull. These
read a workspace's solution capabilities and their narratives, and download
the artifacts of an evidence set.pullreports whether each artifact has an
envelope. -
paramify validators check— runs the behaviour cases in
validators/_cases/through the real ECMAScript engine
(framework/validator_eval/, evaluated undernodebecause Paramify's engine
is JavaScript and Pythonrecannot compile(?<name>...)at all). A case
file pins what a validator must return for a given artifact; a set-level file
combines every validator on an evidence set the way Paramify does
(ERROR > FAIL > PASS), which is the only way to prove anintegrityvalidator
covers its partner — a violation-counting validator passes its own drifted
artifact, because zero matches is its pass condition. Reports vacuous passes
(a PASS reached with a rule that read nothing) and names validators with no
cases. Deliberately not a CI gate: the cases are an authoring aid, and
legitimate evidence-shape changes should not block a merge. -
An
onboard-platformskill integrates a platform that has no fetchers
yet. It researches the API, sets up a sandbox with real data, plans a set of
fetchers and builds each one. -
Illustrated guides for the architecture, run manifest, fetcher contract,
authoring, envelope, config injection, validators, uploader, pipelines,
inventory and issue-report fetchers, KSI mapping, platform onboarding, private
mirror, versioning and releasing. -
azure_app_service_backups(EVD-AZURE-APPSVC-BACKUPS, KSI-RPL-ABO). For
each App Service web app, whether it is actually
being backed up, judged from its restore points: automatic backups plus
successful custom backups.- Observed backups. It reports
latest_restore_point_age_hoursat run time
andlargest_restore_point_gap_hoursinsiderestore_point_window_hours
(default 48).backed_upis true only when the newest restore point is inside
that window.effective_backup_interval_hoursis the observed interval, and
configured_backup_interval_hoursis kept separate. - Recovery point objective. Set the optional
recovery_point_objective_hours
and each app reportsmeets_recovery_point_objective. - Custom backups. It also reports each app's custom backup configuration and
history, and restores made from custom backups within
restore_test_window_days. - Backup storage. It reports the region, replication and version immutability
of the storage accounts custom backups go to. - Exclusions. Function apps and deployment slots are listed in
results.exclusionswith counts and the reason. - Permissions. Reader is enough. The custom configuration needs the opt-in
Microsoft.Web/sites/config/list/action. Without it the configuration is
recorded asnot_authorized(unknown), not as a collection failure. - Checks and limits. The web app list is checked against Azure Resource
Graph, andresults.limitationsstates what the evidence does not cover. SAS
tokens and database connection strings are never written. - Shared module.
_shared/app_service.pynow holds the web-app client and
kind filter thatazure_app_service_configurationalso uses.
- Observed backups. It reports
-
checkov_bicep(EVD-CHECKOV-BICEP) runscheckov --framework bicep,arm
over a cloned repo, typically one hosted in Azure DevOps, using a PAT with
Code (Read). It records Bicep and ARM checks together, each tagged with its
framework, with per-framework counts.A scan that checked nothing never reads as clean. These cases exit non-zero:
- no
.bicepfiles and no ARMdeploymentTemplateJSON (no_files; counts
are null, not 0) - files present but checkov ran zero checks (
no_checks), e.g. resources
declared only throughbr/public:avmregistry modules - files checkov could not parse (
partial; each is listed, and can be
excluded withskip_pathsonce reviewed)
The
org@prefix of an Azure DevOps clone URL is dropped, andclone_token
is optional, so public repos can be scanned.source_type: tfvcreads Azure DevOps TFVC, whichgit clonecannot fetch.
It looks up the project's newest changeset (or the one named in
tfvc_version), then downloadstfvc_pathat that changeset as one zip
through the TFVC items API, using atfvc_tokenPAT with Code (Read). The
evidence recordssource_typeand, inmetadata, the path,changesetand
changeset_date, where a git source records its branch and commit. Git stays
the default. As incheckov_terraform0.2.0, failed checks on
skip_resourcesmatches move toskipped_checks, the effective skip lists
are recorded inmetadata,use_default_skip_checkscan drop the shipped
defaults, andmetadata.branchnames the branch actually scanned. - no
-
web_vulnerability_disclosure(EVD-WEB-VULNERABILITY-DISCLOSURE), the
secondwebfetcher. Given a site or a security.txt URL, it fetches
/.well-known/security.txtand records it verbatim, with its fields parsed per
RFC 9116 and whether it carries an OpenPGP cleartext signature (not verified).
It then follows eachPolicy:link and extracts its text asweb_page_text
does, and each httpsEncryption:link, where an ASCII-armored public key is
read with the standard library (fingerprint, creation, expiry, user IDs,
subkeys). Fixed fields come first for validators:expires_in_days,
canonical_matches_url,contact_emails_in_key,
key_expires_after_security_txt,policies_all_okandpolicy_placeholders
([X]-style blanks left in the policy). Missing REQUIRED fields, a wrong
content type and broken linked pages are recorded as findings. The run fails
only when security.txt itself cannot be collected or parsed. Tested with the
security.txt and key Paramify publishes, served locally; the key's facts match
gpg --show-keys. Also run live against paramify.com. Linked URLs and their
redirects may only reach public addresses; a link to a private, loopback or
link-local address is recorded asblocked_address. No KSI mapping. -
A
zendeskcategory and its first fetcher,zendesk_help_center_section
(EVD-ZENDESK-HELP-CENTER-SECTION). Given a Help Center section URL, it lists
the section's articles through the Help Center API as an anonymous user, so
only public articles count. It followsnext_pageto the end and fails the
run when the collected number differs from the API'scountor any API call
fails. Each article keeps Zendesk's own fields and body HTML, plus plain
text, a text hash, any "Last updated" date in the text, and an anonymous GET
of its page recording whether it was sent to a sign-in page or met a
Cloudflare bot challenge (a challenge leavesall_articles_publicnull,
since public access could not be checked). Fixed fields
come first: article count, newest and oldestupdated_at,
days_since_last_update,all_articles_public, and two Paramify-specific
keyword heuristics for a Secure Configuration Guide
(mentions_top_level_admin,scg_terms) that show keyword presence, not
adequate guidance, plusall_user_segments_everyone(no article is
restricted to a user segment). An empty section is recorded, not failed.
Run live against support.paramify.com's Recommended Secure Configuration
section (4 articles, matching the API'scount). There, Cloudflare
challenged every anonymous article-page GET, soall_articles_public
read null. No KSI mapping. The README's supported services grid lists
Zendesk, using the Simple Iconszendeskmark (CC0) in light and dark
variants (fetchers/logos/zendesk.svgandzendesk-dark.svg). -
A
webcategory and its first fetcher,web_page_text
(EVD-WEB-PAGE-TEXT). It fetches one public URL per target, with no
credential, and records the text the page served. For HTML that is the
visible text, one line per block element, with script, style and head
dropped and navigation and footer kept. For JSON it is the parsed document
plus the body verbatim, and plain text is kept as served. Fixed fields come
first so a validator's first match lands on them: the final URL and each
redirect, the HTTP status and content type, the<title>, alast_updated
date copied verbatim from the first "Last updated" or "Effective date" line
(or the line after a bare label, as www.paramify.com lays it out),
the text length, the body'ssha256, andtext_sha256of the extracted text,
which is the stable change signal because markup and per-request tokens do
not move it. The run fails on a network or TLS
error, any status other than 200, more than 5 redirects, a body over 5 MB, an
unsupported content type, JSON that does not parse, or a page with no text,
which is how a page rendered by JavaScript shows up. The evidence shows what
the URL served when it was collected, not which version of a document is in
force. HTML parsing uses the standard library, so there is no new
dependency; the HTML-to-text and date capture live infetchers/_lib/html_text.py
for any category to import. Tested with synthetic fixtures. Run live only against GitHub
pages, because www.paramify.com was not reachable from the test environment.
No KSI mapping. The README's supported services grid lists Web, with a globe
icon drawn for this repo (fetchers/logos/web.svgandweb-dark.svg). -
jira_change_requests(EVD-JIRA-CHANGE-REQUESTS): change, internal
connection and exception tickets with CCB approver and time, deployment time,
test and security review results, watcher and comment counts, status and field
history, and per-ticket findings (deployed without approval, approved after
deployment, failed tests, missing security review, exceptions open past expiry
or overdue for review). Field names are configurable and resolved by name. -
jira_incident_postmortems(EVD-JIRA-INCIDENT-POSTMORTEMS): incidents with
severity, category, minutes from detection to report against a reporting window
and per-category targets, phase history, linked post-mortems and their action
items, with findings for late reports, missing post-mortems and unassigned or
overdue action items. -
jira_remediation_items(EVD-JIRA-REMEDIATION-ITEMS): the Jira tickets a
required JQL (JIRA_REMEDIATION_JQL) selects as findings, each with owner, due
date, overdue flag and full status history fromPOST /rest/api/3/changelog/bulkfetch,
plus open, closed, overdue, unassigned and days-to-close rollups. Browse Projects only. -
jira_audit_records(EVD-JIRA-AUDIT-RECORDS): the Jira site audit log over a
lookback window (GET /rest/api/3/auditing/record) with actor, time, source address
and changed values, and a rollup of permission, user, group, workflow and scheme
changes. Needs the Administer Jira global permission. -
jira_access_roles(EVD-JIRA-ACCESS-ROLES): Jira site-admin and admin groups
and their members, product access roles, and per project the privileged permission
grants, project role members and the resolved privileged users, flagging privileged
grants made to everyone. Needs the Administer Jira global permission. -
azure_vnet_egress_routing(EVD-AZURE-VNET-EGRESS) — where each subnet's
and network interface's Internet-bound traffic goes, asegress_path: an
appliance, a black hole, a gateway, a NAT gateway, or Azure's implicit default
outbound access (default_outbound_access_effectivereads an unset subnet flag
as on). A default route split into halves (0.0.0.0/1+128.0.0.0/1) counts as
the default, and on a subnet whose default goes to an appliance or gateway every
route straight to the Internet is listed ininternet_bypass_routes. An
appliance next hop is resolved to what holds it (appliance_owner): an Azure
Firewall, an IP-forwarding NIC, or something outside the subscription.
Subnets connected to a Virtual WAN hub with routing intent resolve to the hub's
firewall when the connection propagates the default route. Each subnet also lists
its own and its peers' address ranges that are not forced through the appliance.
Every route table says whether a subnet uses it, and every Azure Firewall carries
its tier, policy, effective threat-intelligence mode (the strictest along the
policy's parent chain) and the subnets routed through it. A spoke VNet is
recognised from its own peerings even when its hub is in a separate connectivity
subscription. The hub is then looked for in up to 50 other subscriptions the
credential can read (VIRTUAL_HUB_SEARCH_MAX_SUBSCRIPTIONS), keeping only that
hub's connections to this subscription's spokes; a spoke still not found reads
virtual_hub_unresolved, notdefault_outbound. Load balancer outbound rules are
not evaluated. Configured routes only:
effective routes need an action Reader does not have. Tested read-only against a
production subscription, where every count matched Resource Graph. -
azure_ddos_protection(EVD-AZURE-DDOS) — every public IP, what holds it,
its virtual network and itseffective_protection:network_protection(a DDoS
plan on the VNet),ip_protection,none,disabled,not_supported(NAT
gateway IPs, which DDoS Protection cannot cover), orunknownwhere the VNet
cannot be resolved. A public load balancer frontend takes its backend pools' VNet.
Also every DDoS
protection plan and the VNets that use it. Virtual WAN hub firewall addresses are
not public IP resources and are not listed. -
Four Azure fetchers: flow logs, AI services, managed identities and API
Management, read-only on the built-in Reader role.azure_network_flow_logs
reports every Network Watcher and flow log (target, retention, storage
destination, Traffic Analytics) and works out which virtual networks and
subnets have their traffic logged, counting subnets with workloads apart from
empty ones and those Azure allows no NSG on; legacy NSG flow logs are marked,
since Microsoft retires them in September 2027. A flow log with no retention
policy keeps records indefinitely, and a filtered VNet flow log is not full
coverage.azure_ai_services_configuration
reports each Azure AI services account (Foundry, OpenAI, Language, Speech,
Document Intelligence) and AI Search service: network exposure, whether API
keys still work, customer-managed keys, managed identity and whether audit
logs are exported; every model deployment with its publisher, where it may
process data (global, data zone or regional) and its content filter and
Prompt Shields protection, judged on the four harm categories because
Microsoft's own default annotates some filters rather than blocking them;
custom filter policies weaker than the default; and every Foundry connection
agents use, with whether it holds a stored secret.azure_managed_identity_configurationreports every user-
and system-assigned identity with what it is attached to, its federated
credentials (GitHub subject breadth, flexible matching expressions,
audiences), its Microsoft Graph application permissions, and its role
assignments judged by what each role's permissions allow (full control,
granting access, reading keys, full data access) rather than by name.
azure_api_management_configurationreports each API Management service's
network exposure, legacy TLS and every switchable cipher (unset switches
follow Azure's documented defaults, which leave the CBC and RSA-key-exchange
suites on), whether legacy management API versions are blocked, certificate
expiry, developer portal sign-up, its subscriptions including the built-in
all-access one, which security policies apply at global, product, API and
operation level, how each API is protected, its backends and whether gateway
logs are exported; policy XML, named-value contents and subscription keys are
never written to the evidence. Adds
azure-mgmt-cognitiveservices, azure-mgmt-msi and azure-mgmt-apimanagement. -
Three Azure fetchers: AI hubs, container workloads and the data tier,
read-only on the built-in Reader role.azure_ai_hub_configurationreports
each AI hub, AI project and Machine Learning workspace: network exposure,
managed virtual network isolation and outbound rules, high business impact,
customer-managed keys, whether system datastores are reached with the storage
key or an identity, every datastore's credential type, every model endpoint's
authentication mode and public access, compute public IPs and SSH, diagnostic
log export, and every connection with whether it holds a stored secret. It
reads ARM JSON because the Machine Learning SDK predates hubs.
azure_container_workloads_configurationreports Container Apps environments
(internal-only or public, mTLS and peer traffic encryption, log destination),
apps and jobs (secrets stored in the app rather than Key Vault, password-based
registry pulls, plain environment variables with secret-looking names,
internet-facing ingress and whether Easy Auth stops an unauthenticated caller
(AllowAnonymous does not), images running a
mutable tag rather than pinned by digest) and Container Instances
container groups (public IP, VNet placement, registry credentials, Azure
Files volumes, logging).azure_data_services_configurationreports MongoDB
vCore clusters (firewall rules judged on how many addresses they admit
together, native versus Entra authentication,
high availability), Data Explorer clusters (open allow-lists, trust of any
external tenant, purge rights, callout policies, principal assignments) and
Event Hubs namespaces (minimum TLS, network rules, shared keys with Manage
rights while local auth is on at namespace and hub level, geo-replication,
event hub retention and capture), each with customer-managed keys and audit
log export. ARM JSON is read through the sharedarm_restclient and log export
through_shared/diagnostics.py. New shared modules:foundry_connections.py
(connection auth types, also used byazure_ai_services_configuration) and
ip_ranges.py(firewall-range width). Adds
azure-mgmt-appcontainers, azure-mgmt-containerinstance, azure-mgmt-mongocluster,
azure-mgmt-kusto and azure-mgmt-eventhub. -
Default custom tags on everything the uploaders create. Every evidence
set, script and validator thatparamify upload,paramify scripts sync
andparamify validators synccreate or maintain now carries two custom
tags: a provenance tag,Automated by Paramify Fetchers, and a service tag
from the category's newdisplay_name(AWS,Okta,SentinelOne).
Tags are added on every run and never removed, so a user's own tags on
those resources are left alone and resources from earlier runs are
backfilled. Both areupload.yamlknobs undertags:— rename the
provenance string, drop either tag, or settags: falsefor none. To
switch the feature off without touching a file, set
PARAMIFY_CUSTOM_TAGS=offin the environment, or pass--no-tagsto any
of the three commands for one run; the Done block says which switch is in
effect. A token without the custom-tags permission gets one warning and
the stage proceeds untagged. Every category file now declares adisplay_name; the category
schema gains the optional field.paramify validators syncreads the
repo'supload.yamlby default, as the other two stages already did. -
azure_redis_cache_configuration, for classic Azure Cache for Redis
(Microsoft.Cache/redis), the sibling ofazure_managed_redis_configuration.
Per cache it reports the minimum TLS version and whether the non-TLS port is
enabled, whether authentication is required at all, whether access keys are
disabled so only Entra ID can be used, public network access, private
endpoints, VNet injection and firewall rules, judged on the distinct addresses
all rules admit together so a split or near-total range is visible. For an assessor it also reports who holds Entra data-access
policies, whether connection and Entra sign-in audit logs reach a destination
(through a new shared_shared/diagnostics.py), the maintenance window, and
replication, zone redundancy (including Azure's Automatic zonal allocation),
geo-replication and persistence, and its summary names the
caches behind each failed expectation.list_keysis never called and
the projection is an allow-list, so access keys and the storage connection
strings in the cache's Redis configuration never reach the evidence. A cache
withoutminimumTlsVersionaccepts TLS 1.0 and is not counted as TLS-only, and
any per-cache read that fails is recorded as unknown. Addsazure-mgmt-redis.
Verified against a live subscription. -
A third fetcher kind,
kind: inventory, for fetchers that list every
asset of a kind for Paramify's Inventory. An inventory is evidence with a
record contract: the payload holdsdata, one record per asset each with a
uniqueunique_asset_identifier, andrecords_included. The record fields
are each fetcher's own. It needs anevidence_setand goes to it through
paramify upload, where an inventory pipeline attached to the set builds
Inventory items. A pipeline reads the file as the whole estate, so the
runner checks every inventory output when it envelopes it and records the
verdict inmetadata.inventory, and the uploader does not send one that is
incomplete: a failed run, a contract break, withheld records, or no records
at all. The TUI lists inventories in their own section. Start from
fetchers/_template_inventory/; seedocs/inventory_fetchers_guide.md. -
Two Azure fetchers for private connectivity, read-only on the built-in
Reader role.azure_private_endpoint_configurationreports each private
endpoint's connection status (Approved, Pending, Rejected or Disconnected),
subnet, private IPs and private DNS zone groups, and for its target resource
whether it still exists and whether public network access is disabled. An App
Service slot is checked on the slot, not its parent site. Per endpoint subnet
it reports whether the subnet's NSG and route table apply to endpoint traffic,
which Azure does only when the subnet's endpoint network policies are on.
azure_private_dns_zone_configurationreports each private DNS zone's virtual
network links and record sets, and checks every IP of eachprivatelinkA
record against the private endpoints that hold it: backed, ip_mismatch, stale
or unverified, with a record that has a dead IP next to a live one reported as
partially backed. The match is on the record's own name, because a public name
derived from the zone is wrong for Key Vault and AKS. Zone names that exist
more than once are found across every subscription the credential can read.
Both record whether Microsoft.Network is registered. A failed lookup is
reported as unknown, never as deleted or stale, and a resource whose
subscription or resource group the credential cannot read is reported as not
visible rather than deleted. Shared Resource Graph paging and the readable-scope
check live in_shared/azure_common.py. -
Four Azure fetchers for the network edge and cache tier, read-only on
the built-in Reader role.azure_managed_redis_configurationreports each
Azure Managed Redis cluster's minimum TLS version, public network access and
private endpoints, and per database the client protocol (Encrypted or
Plaintext) and whether access-key authentication is disabled. Classic Azure
Cache for Redis is not read.azure_dns_configurationreports each public
zone's DNSSEC state and signing keys, and each private zone with its virtual
network links. The DNS SDK has no DNSSEC operation, so it is read with a
direct ARM call, and a 404 "DNSSEC is not enabled" counts as unsigned rather
than as a failed collection.azure_vpn_gateway_configurationreports VPN
gateways, the IKE version, status and IPsec policy of each connection, and
point-to-site protocols and sign-in types. A connection with no custom policy
is reported against Azure's default, whose IKEv2 main mode uses DH Group 2, so
the default does not read as free of weak algorithms. Point-to-site counts only
when a client address pool exists, because the list call returns a placeholder
on gateways that never set it up.
azure_virtual_wan_configurationreports Virtual WANs, whether hubs are
secured by a firewall, and the IKE version on each site link, along with
vWAN VPN sites, point-to-site gateways and server configurations. The DNS,
VPN and Virtual WAN fetchers record whether Microsoft.Network is registered,
so zero resources can be told apart from a provider that is off. A read that
fails (a Redis cluster's databases, a zone's DNSSEC config) or an IKE protocol
Azure leaves empty is recorded as null and counted as unknown, never as
compliant, unsigned or IKEv1. Both VPN
fetchers keep an allow-list of fields, because Azure returns site-link
pre-shared keys in plain text to Reader; no key reaches the evidence.
Verified against a live subscription. -
Evidence samples: one record per fetcher, captured once. A fetcher can
now carry asample.jsonbeside itsfetcher.yamlthat shows what it
collects without anyone running it. Where the payload repeats a record (one
entry per instance, user or subnet), the sample keeps the most filled-in
one, with how many were seen and how often each field was filled in. New
commands:paramify samples status,paramify samples show <fetcher>and
paramify samples backfill, which builds samples from past runs under
./evidence. Backfill replaces emails, person names, logins, account ids,
UUIDs, long hex ids, IP addresses and target values with stable fakes of the
same shape, and does not write a sample in which any identifier from any
scanned file survives. Free text is not rewritten, so a backfilled sample
starts"reviewed": falseand needs a person to read it before it is
committed. See
docs/evidence_samples.md. -
Discovery fetchers (
kind: discovery), a fourth collection kind that
lists what a platform's credentials can see instead of collecting evidence.
The runner writes their output to<run>/discovery/, which neither uploader
reads. Every platform writes the same JSON document
(framework/schemas/discovery_schema.json). Each resource type says whether
it could be read (ok,denied,not_enabled,error, ortruncatedwith
a lower-bound count), so a type that could not be read never looks like one
with nothing in it. Records about people are counted, never listed. The
shared engine infetchers/_lib/discovery.pyhandles paging, retries and the
document, so a platform's fetcher is a table of resources plus its paging. The
first isknowbe4_discovery_beta: account, groups, training campaigns, modules,
policies, phishing campaigns and tests, and user and enrollment counts. It
pages by cursor, since KnowBe4 retires thepageparameter in November 2026.
The second isaws_discovery_beta: about 75 resource types, at least one for every
AWS service an AWS fetcher reads (a test keeps that true), listed in every
enabled region through the aws CLI, plus a count per resource type from AWS
Resource Explorer wherever it has an index, with each (type, region) pair carrying
its own status so a region denied by a service control policy never reads as
empty. One document per target account. Seedocs/discovery.md. -
A Wiz category with twelve fetchers, for Wiz commercial and Wiz for
Government, sharing one GraphQL client and one service account. Eleven are
evidence fetchers:wiz_scan_coverage,wiz_posture_issues,
wiz_infrastructure_vulnerabilities,wiz_container_vulnerabilities,
wiz_cloud_configuration_posture,wiz_host_configuration_posture,
wiz_threat_detections,wiz_file_integrity_monitoring,
wiz_attack_surface_findings,wiz_code_findingsand
wiz_tenant_security_settings. The last five read Wiz modules a tenant may
not have; each checks the tenant's schema at run time, selects only the
fields that exist, and lists the missing ones in the evidence. A missing
scope or licence fails the collection rather than reading as no findings.
The twelfth,wiz_stig_compliance_report, is an issue report: one CSV row per
STIG control, rule and resource for one enabled Wiz framework, for a
CONFIGURATION assessment's pipeline. Wiz's own compliance CSV exists only as
a saved report, and creating or rerunning one is a write, so the rows are
built from GraphQL queries instead, in a fixed column set and sorted order so
the same Wiz state gives the same file. Every result is reported, not just
PASS and FAIL, so a check that moves to ERROR is not read as fixed. Any Wiz
error, Wiz's 10,000-row cap, an empty result, a disabled framework, or a
finding Wiz returns without its control mapping fails the run with no file
written.
None of the twelve writes to Wiz: the client refuses to send a mutation. -
Two more Wiz issue reports,
wiz_issues_reportand
wiz_vulnerability_findings, on the same read-only client and config
names.wiz_issues_reportsends Wiz's own Issues CSV to a CONFIGURATION
assessment. The report is created once in Wiz and scheduled there; the
fetcher finds it byreport_idor exactreport_name, and downloads its
last run only if that run completed withinmax_report_age_hours(default
26). It never creates, edits or reruns a report, so the service account
needsread:reportsand nothing that writes.wiz_vulnerability_findings
pages through every vulnerability finding and writes the legacy Paramify
column layout for a VULNERABILITY assessment, with an optionalproject_id
filter. Both export everything on every run (no delta mode, since a delta
processed with a cycle close resolves every issue it leaves out), and any
read failure, an empty export (unlessallow_empty), or an interrupted
download fails the run with no file written. See
docs/wiz_pipeline_fetchers.md. -
paramify issues uploadnames the cycle an upload landed on, and warns
when newer cycles exist. Uploads land on the assessment's oldest open cycle,
so an old cycle left open takes every new scan while the newer cycles show
nothing. The TUI's Paramify tab shows the same warning. An HTTP 409 now
quotes Paramify's reason instead of assuming no cycle is in progress, since
work aimed at a cycle other than the in-progress one is refused the same way. -
The issues uploader only reads reports inside the run's
issue-reports/.
A report path in_issue_reports.jsonthat resolves outside it, or a report
that is a symlink, is refused and nothing is read, so an edited index cannot
send another file to Paramify as a scan report. -
_issue_reports.jsonis written atomically, and a corrupt one never closes
a cycle. An index that cannot be read is kept beside the new one as
_issue_reports.json.corrupt, and the new index is markedcannot_close:
the uploader processes that run's reports but does not close the cycle,
because the index can no longer show that every target succeeded. The TUI's
Run tab shows why a fetcher failed next to its status, and focus returns to
the Paramify tab when a send finishes, so its keys work without pressing 5
again. -
Evidence sets carry a collection frequency.
evidence_set.frequencyin
fetcher.yamltakes Paramify's evidence frequency values (DAILY,
THREE_DAY,WEEKLY,BIWEEKLY,MONTHLY,QUARTERLY,BIANNUAL,
ANNUAL,NOT_SET) and defaults toTHREE_DAY, so every fetcher has one
without a change to its file. The runner copies it into the envelope's
metadata.evidence_set, and both uploaders send it when they create a set.
Sets that already exist keep the frequency they have. The uploader config can
override it per fetcher, likenameandinstructions. -
docs/pipelines_guide.md: sending scan reports from the TUI, step by step
with three recordings. It covers a manifest for the scanners, pointing each
fetcher at an assessment and choosing its close policy, running and sending
from the Paramify tab, and jobs and manual closes. It explains how cycles
work, lists what each error means, and maps every step to its command. -
paramify issues jobsandparamify issues close.jobslists an
assessment's pipeline jobs and retries or cancels one (--retry,--cancel);
a failed job blocks every job queued behind it on the same assessment until
one of those happens, and nothing does it automatically.closecloses an
assessment's current cycle on its own, after confirmation, for assessments
whose cycle is filled by several runs, and prints how many open issues the
close auto-closed.
The TUI's Paramify tab has both:jlists jobs and retries or cancels one,
Ccloses one of the manifest's assessments' cycles behind a warning. The
reports themselves are sent withi, which replacesctrl+i(most terminals
send the same byte for ctrl+i and Tab, so it never fired). The run console
says when a run left issue reports to send, and the assessment picker no
longer blocks the app while it lists the workspace's assessments. -
Issue-report invocations in the sidecar.
_issue_reports.json(schema
1.1) gainsinvocations: one entry per issue-report invocation, including a
failed one that wrote no file and an entry that raised before running. Before
this, a failed target that wrote nothing left no trace, and a run missing one
framework's report looked complete. -
A Splunk category with seven fetchers, one Splunk Enterprise deployment per
target, each with its own management URL (port 8089) and bearer token.
splunk_index_retention(retention, archive-or-delete on freeze, data
integrity control),splunk_index_activity,splunk_log_source_freshness,
splunk_data_inputs,splunk_alert_rules(Splunk Web's own alert definition,
with run and fire history from_internaland_audit),
splunk_alert_deliveryandsplunk_role_index_access(effective index access
after inheritance and denials, delete rights, dormant accounts). This closes
KSI-MLA-ALA. Every fetcher is acollect(client, config)function handed to a
sharedrun(), which checks the token's capabilities first and fails the
collection, rather than publishing a partial list, when Splunk returns less
than everything: acountcut short ofpaging.total, a search answered with
a warning, or an index list that disagrees withindexes.confand the search
peers. Tested against Splunk Enterprise 10.4.3, where a role with no
capabilities saw 1 of 4 users and 127 of 176 saved searches, and stockadmin
12 of 26 roles, each time with apaging.totalthat agreed. The collection role
is infetchers/splunk/README.md. Splunk Cloud is not yet supported. -
A Better Stack category and its first fetcher —
betterstack_public_status_page, which GETs a public status page's own
/index.jsonand records it as evidence. No credential: a Better Stack status
page publishes that document to anyone who can reach the page, so the fetcher
sends nothing and the category declares no secrets. The response is kept
verbatim —dataandincludedexactly as the page returns them, nothing
removed, renamed, recomputed or flattened — alongside the URL actually
fetched and a collection timestamp. The one field added to it,
lowest_reported_availability, is a selection rather than a calculation: it
names the component reporting the lowest availability and copies that value
out of the response unchanged, so the evidence carries the same figure the
status page UI shows (0.999915 reads there as "99.991% uptime"). It exists
because a ParamifyMATCH_GROUPrule reads one capture group while the
number of components varies per page — naming the weakest component gives
such a rule one field to compare that still speaks for the whole page. Ties
break on the lowest resource id so an unchanged page selects the same
component every run, and the field is omitted rather than invented when no
component reports a numeric availability. Completeness is enforced rather
than assumed: every id named underdata.relationshipsand every status
update a report references must be present inincluded, and a shortfall
fails the collection instead of producing a populated-looking partial
payload, as do a non-200, a body that is not JSON, and a body that is not a
status page document. The fetcher fans out per status page and is generic —
it knows nothing about any particular page — andfetcher.yaml's
evidence_set.instructionsstates plainly what the evidence cannot show:
that Better Stack'savailabilitycounts hard downtime only, with degraded
and maintenance periods excluded from it, and that/index.jsoncarries no
monitor configuration and so proves nothing about check frequency, what is
actually probed, or monitoring regions. -
A validator for the Better Stack status page set
(validators/betterstack/, with behaviour cases undervalidators/_cases/).
Oneconfigurationcheck: aMATCH_GROUPrule comparing a component's
reportedavailabilityagainst a 0.999 baseline, anchored on the shape of a
status_page_resource rather than on any component's name, so it holds for any
customer's page. Collection health stays where it belongs, in the shared
validators/common/collection_succeeded.yaml, which the set is listed on. -
azure_app_service_plans— every App Service plan with its SKU name and
tier, instance count (sku.capacity), zone redundancy and scaling settings,
plus the web apps and function apps running on it (joined by
server_farm_id). App Service availability is a property of the plan, not the
app, so nothing in the existing Azure evidence showed whether an app survives
losing an instance or a zone. The summary counts zone-redundant plans,
multi-instance plans, single-instance plans, and plans on a no-SLA tier (Free,
Shared); consumption and elastic plans are flaggedplatform_scaled, since
their capacity is not a fixed instance count. Reader-only; evidence set
EVD-AZURE-APPSVC-PLANS. -
azure_defender_regulatory_compliance(EVD-AZURE-DEFENDER-REG-COMPLIANCE)
— Defender for Cloud's regulatory compliance grading, nested standard → control
→ assessment with passed / failed / skipped counts at each level, and headline
keys for the Microsoft cloud security benchmark (mcsb_failed_controls,
mcsb_failed_control_names, ...).azure_defender_assessmentshas the same
results flat, one row per resource and check; this groups them by benchmark
control. A subscription with Microsoft.Security unregistered, or with no
Standard-tier Defender plan (the service refuses regulatory compliance on the
free tier), reports it asregulatory_compliance_statusand exits 0. -
azure_policy_compliance(EVD-AZURE-POLICY-COMPLIANCE) — what the assigned
Azure Policies found, whereazure_policy_assignmentsonly says what is
assigned. Fromazure-mgmt-policyinsights(a new pinned dependency of the
azureextra): the subscription-wide and per-assignment counts of
non-compliant resources and policies frompolicy_statessummarize, a bounded
list of the non-compliant (resource, policy) records
(max_non_compliant_resources, default 1000, with a truncation flag), and the
remediation tasks with their deployment counts. The evaluation window the
service applied is recorded, andresources_evaluatedtells "nothing
non-compliant" from "nothing evaluated". -
A Jira category, and its first fetcher:
jira_site_inventory. One
evidence set covering a Jira Cloud site end to end: deployment info, every
project the account can browse (type, lead, issue types, issue count), the
issue types, priorities and statuses in use, Jira Service Management service
desks where the site has JSM, and every issue updated in the lookback window
(90 days, or an explicitjql) with rollups by project, type, status and
priority, time to resolution, and separate change and incident counts.
Account email + API token, declared once on the category; no Jira admin
needed. Seefetchers/jira/README.md. -
A fanout target editor in the TUI (
ton the Manifest tab). A fanout
fetcher runs once per target, so its targets are the run plan — but the page
showed only how many there were, and there was no way to change one: fixing a
typo'd account id meant removing the target and retyping every field.tnow
opens the fetcher's targets as a table, withaadd,eedit, andxremove
on the highlighted row. Fields come from the fetcher'starget_schema, so the
form is whatever that fetcher actually takes, per-target secrets included. -
paramify manifest set-target <fetcher> <index> k=v …— the CLI half of
the editor. A replace rather than a merge, so a field cleared in the form is
actually cleared; the target's per-target secrets are preserved unless
--secretis passed. -
Upload artifacts through the evidence set's channel. Where a set has one,
the uploader now sendschannelIdwith the artifact: an artifact uploaded
outside a configured channel does not count toward validation on the solution
capability. No configuration — a set with a channel uses it, a set with none
uploads unchanneled exactly as before. A set with more than one channel errors
that file rather than guessing, until the rules for choosing exist. The channel
is recorded per file inupload_log.json, since the API does not report it back
on an artifact. Seedocs/uploader_guide.md. -
A default uploader config at
./upload.yaml. Every upload stage falls back
to it when no--configis given, which is how the TUI — which passes no path —
reaches base_url and the per-fetcher overrides at all. Both front-ends now name
the config in use before a batch runs. -
A central
validators/registry. A validator is now a first-class,
deduplicated object — one file atvalidators/<category>/<key>.yaml, checked
against the newframework/schemas/validator_schema.json. It carries the
Paramify fields as native YAML and owns its side of the fetcher link through an
evidence_setslist ofreference_ids, so a validator that applies to four
fetchers is one file naming four sets rather than four copies drifting apart.
A fetcher's validators are the reverse lookup on itsevidence_set.reference_id.
Seedocs/validators_guide.md. -
Validator sync —
paramify validators syncandparamify upload --with-validators(engine inuploaders/paramify_validators/). Pushes registry
validators to Paramify (POST /validators) and CONNECTs each to its evidence
sets. The validators shipped here are templates that customers tune in their own
instance, so the sync is create-or-skip: an existing validator is never
modified unless--updateis passed, association happens on create only, and
--dry-runpreviews without writing. The per-instance Paramify id is resolved
at sync time and cached in a gitignored.paramify/lock — never written back
to the shared registry. -
azure_entra_service_principals, the tenant's non-user accounts. The
Azure category saw app registrations but not service principals, so a managed
identity, a principal for another tenant's app, or a secret held directly on a
service principal was invisible. Every service principal is now reported with
its type, its owner (Microsoft first-party, this tenant, or another tenant) and
every credential it can present, including federated identity credentials on
this tenant's applications. The summary counts managed-identity, federated,
certificate-based and secret-based principals, and credentials that are
expired, expire within 30 days, or never expire. It makes two paged Graph reads
per tenant, never one per principal, and needsApplication.Read.All. -
azure_entra_authentication_policyshows which sign-in methods the tenant
allows.azure_entra_mfa_statusshows what users have registered, but not
whether the tenant still accepts SMS alongside the FIDO2 keys they registered.
Every method in the authentication methods policy is reported with its state,
targets and settings. SMS and Voice are flagged weak, and FIDO2 (passkeys
included) and X.509 certificates phishing-resistant. The fetcher also reports
the registration campaign, the policy's migration state (legacy MFA/SSPR
policies still apply until it completes), security defaults, and Entra password
protection (custom banned-password list, smart lockout). It reports the Entra
ID licence tier too, because a custom banned-password list is only enforced
with P1. NeedsPolicy.Read.AllandDirectory.Read.All. Anaz login
session cannot read the methods policy, because the Azure CLI's client is not
pre-authorized forPolicy.Read.All. -
azure_resource_inventory— an automatically generated inventory of every
resource in the subscription, from one Azure Resource Graph query over the
Resourcestable: id, name, type, location, resource group, tags, SKU, kind and
provisioning state, with counts by type and by location and the number of
untagged resources. The per-service fetchers each see only their own resource
types, so nothing listed the subscription as a whole. The query is paged to the
end; a truncated result, or a page count that disagrees with Resource Graph's own
total, is a collection failure rather than a shorter inventory. Adds the
azure-mgmt-resourcegraphdependency to theazureextra. -
azure_log_analytics_workspaces— every Log Analytics workspace with its
SKU, workspace retention, daily ingestion cap, public network access for
ingestion and query, access control mode
(enableLogAccessUsingOnlyResourcePermissions), and whether shared-key auth is
disabled. Per-table retention is summarized as counts by plan and retention
plus the minimum across tables, with only the tables that override the
workspace default, or are custom, listed by name — a workspace carries several
hundred built-in tables. Also the workspace's data export rules and whether
Microsoft Sentinel is onboarded (the SecurityInsights solution is enabled).
Nothing in the Azure category recorded where logs are kept or for how long.
Adds theazure-mgmt-loganalyticsdependency to theazureextra. -
Seven Microsoft Sentinel and Log Analytics log-integrity fetchers. Per
Sentinel workspace (SENTINEL_WORKSPACES, or every onboarded workspace in the
subscription):azure_sentinel_data_sources(connectors, plus which tables
and alert products actually ingested in the window),
azure_sentinel_analytics_rules,azure_sentinel_incidents(status,
classification, time to close) andazure_sentinel_automation_rules. Per Log
Analytics workspace, Sentinel or not:azure_log_analytics_query_audit
(whether queries are audited, and weekly human vs app query counts) and
azure_log_analytics_deletion_rights(who, active or PIM-eligible, can purge
or delete log data, delete the workspace or shorten its retention, and
whether through a wildcard role). Per subscription:
azure_log_storage_immutability(immutability policy, legal hold and soft
delete on the containers Azure Monitor writes logs to; only a Locked policy or
a legal hold counts as protected). They share_shared/azure_rest, which
calls ARM, Graph and the Log Analytics query API directly, since the only
stable Sentinel SDK pins a 2021 API version, and retries throttling and
transient server errors, honouring Retry-After. Analytics rules are listed at
the stable and the preview API and merged, since NRT rules exist only in
preview. Data older than a table's retention is reported as not retained,
never as missing. -
Three Azure SQL Managed Instance fetchers. Nothing in the Azure category
covered Managed Instance, which has its own API surface apart from SQL
Database. All three are Reader-only and fan out per subscription.azure_sql_managed_instance_backups(EVD-AZURE-SQLMI-BACKUPS): short- and
long-term retention for each database, the instance's backup storage
redundancy (requested vs current, so a pending change shows), compute zone
redundancy, and the instance failover groups with their failover policy and
instance pairs. Azure lists a failover group once from each region it spans,
each with its own resource id; the fetcher merges them into one record.azure_sql_managed_instance_configuration(EVD-AZURE-SQLMI-CONFIG): the
public data endpoint, proxy override, minimum TLS version, private endpoint
connections, the Entra admin and Entra-only authentication, and whether a
diagnostic setting exportsSQLSecurityAuditEvents. That shows the audit
route only; whether a server audit is on is set in T-SQL, which no ARM call
can read.azure_sql_managed_instance_encryption(EVD-AZURE-SQLMI-ENCRYPTION): the
TDE protector (service-managed or Key Vault key, auto-rotation) and TDE
state for each database.
Any system database the API lists is flagged and left out of every count. A
failed call leaves every flag and total derived from it null rather than
false or 0, so an unanswered call never reads as "off". -
azure_resource_diagnostic_settings(EVD-AZURE-RESOURCE-DIAG-SETTINGS)
— resource-level logging across the whole subscription.azure_diagnostic_settings
reads only the subscription's activity log export, and the per-service fetchers
only their own types, so nothing showed which resources' logs go nowhere. Every
resource Resource Graph lists, plus the blob, file, queue and table services
each storage account's kind has, is checked for the log categories its type emits and the
diagnostic settings that export them:has_enabled_log_setting,
destination_types,sends_to_log_analytics,uncaptured_log_categoriesand
captures_all_audit_categories. The summary gives log-setting and Log
Analytics coverage of the log-emitting resources, with the uncovered ones
counted by type; metrics-only and unsupported types are outside the
denominator. A row count that disagrees with Resource Graph's total, or any
read error other than an unsupported type or a since-deleted resource, fails
the run. OneResourceTypeNotSupportedanswer covers the rest of that type.
Reader-only; about 14 resources a second, timeout 3600s. -
azure_alert_rules(EVD-AZURE-ALERT-RULES) — every metric alert, log
search alert and smart detector rule, resolved through its action groups to
the receivers it notifies and checked against alert processing rules that
suppress or add notifications. A processing rule scoped narrower than the
alert is a partial match. Per rule:routes_to_enabled_receiver,
routes_to_person(email, SMS, voice, app push, ARM role or ITSM; webhooks,
Logic Apps, Functions, runbooks and Event Hubs may or may not reach anyone),
suppressedandreaches_human, each true, false or null when unknown. The
summary counts unknown verdicts apart rather than as "no". Read through raw ARM REST, sinceazure-mgmt-monitor6.x drops
nextLinkon metric alerts and action groups and reads log alerts in the 2018
schema. Reader-only. -
azure_change_provenance(EVD-AZURE-CHANGE-PROVENANCE) — every
control-plane change in the subscription over Resource Graph's 14 days of change
history, attributed by correlationId to the ARM deployment that made it, to a
configured pipeline identity, or to a direct edit. A deployment a person ran,
from the CLI or through the Portal's create wizards, ismanual_deploymentand
does not count as a redeployment. Changes Azure records with no caller take the
origin of the nearest named change to the same resource, then of a deployment
whose run and outputs cover them, and are otherwiseplatformor
unattributed. The summary givespercent_redeployed, a
percent_redeployed_conservativethat counts unattributed changes against it,
direct edits by client, caller and resource type, and the direct changes in
resource groups deleted since, whose deployment history went with them.
Reader-only. Maps toKSI-CMT-RMV. -
azure_residual_resources(EVD-AZURE-RESIDUAL-RESOURCES) — leftovers in
the subscription: unattached disks, NICs, NSGs, public IPs and NAT gateways,
disconnected private endpoints, private DNS zones linked to no network, App
Service plans hosting nothing, empty resource groups, user-assigned identities
with no attachment, role or federated credential, stopped or long-untouched
deployment slots, and role assignments whose principal Entra ID no longer has.
Cross-tenantForeignGroupprincipals are never reported deleted, and the run
fails rather than guess when Microsoft Graph is signed in to a tenant other than
the subscription's. Needs Reader, plus Microsoft GraphDirectory.Read.All
(admin-consented) when it runs as a service principal. Maps toKSI-SVC-PRR. -
Three Azure Application Gateway fetchers. All three are Reader-only and
fan out per subscription. They share_shared/app_gateway.py, which fails
the run whenlist_allreturns a different number of gateways than Resource
Graph counts. Resource Graph can lag behindlist_all, so a mismatch is
counted once more after 30 seconds before the run fails.azure_app_gateway_tls_listeners(EVD-AZURE-APPGW-TLS-LISTENERS): every
L7 and L4 listener, with its protocol, a public or private frontend,
whether a plain-HTTP listener only redirects to HTTPS, and the effective
minimum TLS version along with where it comes from. The source is the SSL
profile, then the gateway'ssslPolicy, then the default predefined policy.
An L4 Tcp listener always counts as plaintext, even when TLS passes through
it to the backend.azure_app_gateway_backend_tls(EVD-AZURE-APPGW-BACKEND-TLS): every
backend setting, with its protocol, whether the certificate chain and SNI
are validated, its trusted roots and its probe protocol, plus which rules
use it. Only settings that are in use count towards the totals.azure_app_gateway_waf(EVD-AZURE-APPGW-WAF): the WAF that applies to
each HTTP(S) listener. A path rule's policy overrides the listener's, which
overrides the gateway's, which overrides the legacy configuration. The
fetcher reports whether that WAF is enabled in Prevention mode, and each
policy's disabled managed rules, exclusions and rate-limit rules. A policy
in another subscription is read with a direct GET. If the identity is
refused (401 or 403) or the policy no longer exists (404), the policy is
recorded as unreadable, the listener's result is null rather than
"enforcing", and the run does not fail. Any other error does fail it.
These fetchers were run against a live subscription with one gateway. SSL
profiles, attached WAF policies and L4 listeners have not yet been
exercised against live data.tests/test_azure_app_gateway_verdicts.py
covers those paths against modelled gateways.
Changed
-
New evidence sets start their collection schedule on the day they are
created. Both uploaders (paramify_evidenceandparamify_scripts) now send
startDate(today,YYYY-MM-DD) alongsidefrequencywhen they create an
evidence set, so the TUI andparamify uploadno longer leave it unset.
Existing sets are reused unchanged. -
aws_config_conformance_packsmaps rules to FedRAMP Low and Moderate and
to NIST 800-53 Rev. 5 controls (0.1.0 → 0.5.0). Deployed rule names change on
each redeploy, so each one now resolves to its stable source identifier. The
raw AWS response stays in the evidence. Adds Moderate pack support and a
control-to-rule view. -
aws_detect_new_aws_resourcefinds rules and topics with other names, and
accepts event-driven rules (0.1.0 → 0.2.0). New optionalrule_nameand
topic_nametarget fields keep the old defaults. Event-pattern rules no
longer fail the polling-interval check. Addsvalidation_results.rule_checks. -
Azure SDK dependencies.
azure-mgmt-resource-policymoves from 1.0.0b3 to
1.0.0, andazure-mgmt-networkstays below 34. Theazureextra adds
azure-mgmt-redisenterprise,azure-mgmt-dns,azure-mgmt-privatednsand
azure-mgmt-resource-deployments. -
azure_policy_compliance0.2.0: what was evaluated, whether remediation took,
and what keeps coming back.- Each listed non-compliant state now carries
evaluation_details: the
expressions Azure Policy evaluated, with field path, operator, actual value and
target value, plus any ifNotExists lookup. - The service returns these only per resource, so they are read for at most
max_evaluation_detail_resources(default 100) of the listed resources. The
resources with the most listed states are read first. evaluation_details_statusgives the reason a state has no details. It is
collectedonly when Azure returned an evaluated expression or an ifNotExists
lookup; a state returned without either isnot_returned, as Resource Provider
mode policies (Microsoft.KeyVault.Dataand the like) always are. A state the
resource's query did not return at all isnot_in_detail_result. A
per-resource read that Azure refuses (a 403, or an AuthorizationFailed error
code, which Policy Insights has returned with a 409) is retried once, 5 s
later. A run makes at most 3 retries, and none after a retry is refused again,
since the refusal is then systematic. Still refused, that resource's states
arenot_authorized, with the error code and refused action (never the
principal) inevaluation_details_reason, and it is counted in
evaluation_details_not_authorized_resources. It does not fail the run, since
the details only enrich the states; any other error on that read, a 404 or a
5xx included, still does.
evaluation_details_truncatedsays when not every listed resource was read.- Each remediation task's per-resource deployments (status, error, times) are
listed inremediation_deployments, up tomax_remediation_deployments
(default 1000), and each is checked against its resource's latest compliance
state, withcurrent_state_evaluated_atandreevaluated_since_deployment.
The states are read only for the remediated resources. New summary keys count
remediated resources (remediated_resources_listed) and those now compliant,
now non-compliant, and whose succeeded remediation is now non-compliant. These
count each resource once, and its latest deployment decides; tasks re-run on
one resource add deployments, not resources. The non-compliant counts include
resources that were fixed and later drifted back;non_compliance_recurrence
shows the drift when that pair was read.now_compliantis null when the state
was evaluated before the deployment last changed, and that deployment counts as
remediated_resources_not_yet_reevaluatedinstead. When either time is
missing,now_compliantcomes from the state. Azure deletes a task 60 days
after its last change, so this covers about the last 60 days at most. - From 30 days of history, the summary counts (resource, policy) pairs
non-compliant throughout and pairs non-compliant only some of the time.
history_truncatedis also set when fewer groups come back than the service
counts. When the history read fails,history_collectedis false and these
counts are null rather than 0. - For up to
max_recurrence_pairs(default 100) of the part-time ones,
non_compliance_recurrencecounts the separate non-compliant episodes and how
many times the pair fell back out of compliance.recurring_non_compliant_pairs
counts pairs with more than one episode. - Every bounded list has a truncation flag; a limit of 0 skips that list and
leaves its flag false. Existing fields are unchanged. Still Reader-only. Also
mapped toKSI-SVC-EIS. runtime.timeoutis 3600 seconds: the added queries run serially, and a
production run took about 8 minutes, near the runner's 600-second default.
- Each listed non-compliant state now carries
-
azure_network_security_groups0.3.0: east-west segmentation. Each workload
subnet now says which other subnets in its VNet can reach it on every port
(east_west_segmentation:segmented,partial,unsegmented,undetermined
orno_peer_subnets, with the subnets named on each side). Both subnets' NSGs
are walked in priority order, default rules included, so a VNet relying on the
platform's AllowVnetInBound readsunsegmented. An allow scoped to an
application security group, or an NSG in another subscription, gives
undeterminedrather than a guess. A rule's port ranges are merged, so a deny
split across0-1023and1024-65535covers every port. Platform subnets are left out. Subnets gain
address_prefixesandplatform_subnet, and rules gain their application
security group ids. NIC-level NSGs, traffic within a subnet and peered VNets are
not considered. -
The TUI keeps evidence and scan reports apart. The catalog and the
add-fetcher picker group by kind first (Evidence, Scan reports) and
platform second, so a platform that ships both, like Wiz, no longer lists
them in one folder. Highlighting a section heading explains what that kind
is and how it is sent. A fetcher's contract states its kind and where its
output goes: the evidence set, or for a scan report the type of assessment it
needs, its format, and how it is sent. The manifest table replaces themode
column (thetargetscolumn already shows—for a fetcher that runs once)
withkind, and adds a lastsends tocolumn: the evidence set, or the
assessment and close policy, orno assessment — press A. The manifest's
detail pane shows the same. The TUI now says "scan report" wherever it said
"issue report";kind: issue_reportinfetcher.yamlis unchanged. The
catalog descriptor (paramify catalog --json) gains anevidence_setblock
andissue_report.format. -
TUI fixes found while recording that walkthrough. On a manifest with no
evidence and no scripts, nothing on the Paramify tab held focus, so its keys
did nothing. Enter in a picker's filter box now takes the highlighted option.
The confirm dialog wraps long messages instead of cutting them off, which had
hidden the auto-close warning. The jobs list shows each job's assessment by
name and its error. A queued or running job is marked as waiting, not failed. -
paramify uploadandparamify issues uploadpick the newest run of
their own kind when no run is named, not simply the newest run: evidence
and pipeline manifests usually share an output directory, and each command
used to pick up the other's run (issues uploadthen failed with "this run
collected no issue reports" while the scan run sat beside it). Both take
-f MANIFESTto use only that manifest's runs, reading its output directory
when--output-diris not given. The standalone uploaders and each panel of
the TUI's Paramify tab choose the same way; the TUI scopes to the active
manifest's runs once it has any. -
AWS fetchers finish in large accounts (67 fetchers, minor version
bumps). Many made several AWS CLI calls, and started severaljqprocesses,
per resource, so an account with thousands of snapshots, roles or security
groups ran past the runner's 600-second timeout. They now read each resource
type with one paginated or batch call where AWS offers one, and build their
records in a singlejqpass. Measured against an account with 2,925 EBS
snapshots, 912 IAM roles, 1,003 customer-managed policies and 1,009 security
groups:ebs_snapshot_status,iam_roles,iam_policiesand
security_groupseach timed out before and now finish in 2–26 seconds with
2–4 calls, and every fetcher that finished before and after produced
byte-identical evidence. Fetchers with no bulk form of their per-item call
(S3 bucket settings, SQS queue attributes, KMS key details) still make one
call per item, but no longer start a process or rewrite the output per item.
The evidence-set instructions in eachfetcher.yamlnow list the commands
the fetcher actually runs. -
azure_network_security_groupsshows outbound posture and NIC coverage
(0.1.0 → 0.2.0). Only an NSG's custom rules were read, so the platform's
default AllowInternetOutBound — which permits all Internet egress unless a
custom rule overrides it — never appeared, and every NSG looked like it said
nothing about outbound traffic. Each NSG now also carries its
default_security_rules, and the summary adds outbound counts, including
unrestricted_internet_outbound_groups(rules evaluated in priority order).
Network interfaces are now listed too, with the NSG on the NIC or inherited
from its subnet;nics_without_any_nsgcounts the ones with neither, which
subnet coverage alone could not show. Rules gainpriorityand destination
address fields. Existing fields and summary keys are unchanged. -
azure_app_service_configurationshows access restrictions and diagnostic
settings (0.1.0 → 0.2.0). The access-restriction rules for the main site and
the SCM (deployment) site were already returned by the per-app configuration
call but dropped from the evidence, so nothing showed whether an app accepts
traffic from anywhere. Each app'sconfigurationnow carries both rule lists,
their default actions,scm_ip_security_restrictions_use_main, and an
evaluatedmain_site_allows_all_traffic/scm_site_allows_all_traffic; the
summary counts the apps where either site lets every caller through. Each app
also gainsmonitor_diagnostic_settings(onediagnostic_settings.listper
app, still Reader-only), counted asapps_with_diagnostic_settingsand
apps_with_enabled_logs. Existing fields and summary keys are unchanged. -
azure_rbac_role_assignments0.2.0 collects resource-group and resource-level
grants. The fetcher listed withatScope(), which returns only assignments at
the subscription scope and above, so a role granted on one resource group or one
resource — a Contributor on a single Key Vault, a data role on a storage account —
never reached the evidence. A second, unfiltered listing now fills
results.role_assignments_below_subscription(the unfiltered result minus the
atScope()set, same record shape), with newbelow_subscription_*and
all_scopes_*summary keys, includingall_scopes_assignments_by_scope_level.
Every existing key is still computed over the subscription-and-above set and
means what it did. Also mapped toKSI-CNA-DFP,KSI-IAM-JITand
KSI-MLA-ALA. -
azure_policy_assignments0.2.0 resolves each assignment's policy effect.
enforcedonly ever meant enforcementMode Default, so an enforced assignment of
an audit-only initiative read the same as one that denies. Each assignment now
carriespolicy_effects— the effect of its definition, or of every member of
its initiative, with[parameters('effect')]-style effects followed through the
assignment's parameter value, then the initiative's default, then the
definition's default, and the source of each recorded — pluseffect_class
(enforcing/audit_only/disabled/unresolved) andactually_enforces
(an enforcing effect — deny, denyAction, modify, deployIfNotExists, append — under
enforcementMode Default). New summary keys:actually_enforcing_assignments,
audit_effect_only_assignments,assignments_by_effect_class,
member_policy_effect_countsand others; the existingaudit_only_assignments
still means DoNotEnforce. Also mapped toKSI-CNA-IBPandKSI-MLA-EVC. -
azure_key_vault_configuration0.2.0 records each vault's audit logging.
A vault's data-plane operations — who read or changed which key, secret or
certificate — are logged only if a diagnostic setting exports theAuditEvent
category, and the evidence did not say whether one did. Each vault now carries
its diagnostic settings,audit_logging_enabled(true when a setting with a
destination exportsAuditEventdirectly or through theaudit/allLogs
group), and the destinations the audit events reach. The summary adds
vaults_with_audit_logging,vaults_without_audit_logging,
vaults_audit_logging_unknown(the read failed — neither logged nor unlogged)
andaudit_logging_percentage. Existing fields are unchanged. The fetcher now
also maps toKSI-MLA-LET, and toKSI-CNA-MAT,KSI-CNA-RNTand
KSI-SVC-SIN, which its network and SKU fields already evidenced. -
azure_activity_log_alerts0.2.0 says whether an alert reaches a person.
A rule with an action group counted as monitoring even when the group was
disabled, had no enabled receiver, or an alert processing rule removed its
notifications. Each rule now carriesresolved_action_groups(enabled state
and enabled receivers; webhook host only, phone numbers masked),
routes_to_enabled_receiver,routes_to_person,suppressedand
reaches_human, decided as inazure_alert_rules(true, false, or null when
unknown), from raw ARM reads of action groups and processing rules. A second
map,change_operation_coverage, covers App Service create / update,
configuration, deployment and slot swap, at site and slot level, counting
only alerts that reach a person (null where only one that might covers it);
monitored_operation_coverage_reaching_humandoes the same for the existing
map.action_groupsandmonitored_operation_coverageare
unchanged. -
azure_resource_inventory0.2.0 shows each resource's Defender and Policy
coverage. Two more Resource Graph queries read Defender for Cloud assessments
(SecurityResources) and Azure Policy states (PolicyResources). They are joined
to the inventory by resource id in the fetcher, because Resource Graph allows one
cross-table join per query and pagessummarizeonly to its first page. Each
resource gainsdefender_assessments,defender_unhealthy,
defender_unhealthy_high_severity(High or Critical),policy_evaluationsand
policy_non_compliant; a result on a child resource, such as a subnet, counts for
the inventoried parent. Only Healthy and Unhealthy assessments count:
NotApplicable ones did not happen (off by policy, for instance) and are counted
apart, indefender_not_applicableanddefender_not_applicable_total; an
assessment with a missing or unexpected status counts in neither, but in
defender_unknown_status_total. The
summary listsresources_without_defender_assessment
andresources_without_policy_evaluation. A resource counts there only when the
service covers another resource of its type in the subscription. Types it covers
on no resource are listed apart, with counts, intypes_without_defender_assessment
andtypes_without_policy_evaluation, so a type Defender has no recommendations
for is not a gap.all_assessable_resources_assessed_by_defenderand
all_evaluable_resources_evaluated_by_policyare false when the service covers
nothing at all, and on an empty inventory, which is what Resource Graph returns
to a caller without Reader.defender_coverage_collectedand
policy_coverage_collectedsay whether each coverage query was read to the end. Also new: coverage percentages, unhealthy High/Critical and
non-compliant counts on inventoried resources, and
resources_covered_by_neither_by_type(uncovered resources of every type, gaps
and never-covered types alike). Assessments and policy states about anything not
inventoried (the subscription, resource groups, deleted resources) are counted
apart, withdefender_unhealthy_outside_inventory,
defender_unhealthy_high_severity_outside_inventory,
defender_unhealthy_without_severity_outside_inventoryand
policy_non_compliant_outside_inventory, so the inventoried counts plus these add
up to the totals; many High recommendations (owner MFA, Defender plans) are about
the subscription itself. Assessments of AWS, GCP or on-premises machines (a
resourceDetailssource other than Azure) always count outside, by source.
results.coverage_queriesgives each query's page count, reported total and
outside_inventory_by_target, the same counts for those results by the type of
what they target (never its name). Every
query is paged to the end; a coverage query that fails or comes back truncated
is a collection failure, and that service's counts stay null rather than reading
as uncovered. Also mapped toKSI-MLA-EVC,KSI-CNA-EISandKSI-SVC-EIS.
Existing fields and summary keys are unchanged. -
azure_defender_plans0.2.0 records each plan's sub-plan, enablement time
and inheritance. Each plan now carriessub_plan(ServersP1/P2,
DefenderForStorageV2,PerKeyVault, …),enablement_time(the last time it
was set to Standard;nullwhen Azure does not report it, which can be the case
for every plan, soenabled_plan_enablement_timescan be allnulland is not a
reliable signal for a validator),resources_coverage_status,enforce,
deprecatedandreplaced_by, andinherited/inherited_fromwith
inherited_from_scope(management_group,subscription,resource_groupor
resource), all from the samepricings.listcall. Azure documents inheritance as reported for
resource-level pricing only, so at subscription scope it readsnulland
plans_reporting_inheritancesays whether any plan reported it. New summary
keys:enabled_plans,enabled_plan_sub_plans,enabled_plan_enablement_times,
enabled_plans_not_fully_covered(an enabled plan with no coverage status is
listed asunknown, so it never reads as covered),plans_not_enabled
(deprecated plans left out),deprecated_plans,deprecated_plans_enabled,
enforced_plans,inherited_plansandplans_inherited_from_management_group.
The summary does
not judge which plans a system needs. Existing fields and summary keys are
unchanged. -
azure_diagnostic_settingsalso maps toKSI-MLA-OSM, which its
subscription activity-log export already evidenced. -
KSI-SVC-PRRcounts as config-evidenceable. Its residual elements are a
state of the environment a fetcher can show, soframework/reference/ksis.yaml
marks it evidenceable anddocs/ksi_mapping.mdlists it as covered by
azure_residual_resources: 35 of 37 evidenceable indicators. -
The TUI saves manifest edits as they are made. Adding a fetcher, editing an
entry, adding / editing / removing a target, picking an assessment, removing an
entry, and changing the output dir all write the file immediately. Edits used
to live in memory untils, with nothing on screen to say the file and the
view had diverged — so quitting, or not knowing the key, discarded the work.
sstill saves on demand, which is also how a write that failed is retried: a
save that cannot happen reports why and keeps the edit rather than losing it
along with the file write. -
Validator authoring: guards are written positively, and collection health is
its own validator.validationRules[].dispositionis accepted and then
discarded by the REST API, so a rule authored in the negativeERRORform
degrades into a contradictory pass-requirement and the validator can never
pass — measured returning FAIL on every input, including compliant evidence.
Collection health now lives once invalidators/common/collection_succeeded.yaml
(listing every evidence set that wants it) with plain PASS rules; compliance
validators no longer carry anexit_codeanchor, so their capture groups start
at 1. Migration when the API honoursdispositionis an inversion of two rules
in that single file. -
Every validator opens with a presence rule, and count-based validators get an
integritypartner. AMATCH_GROUPcomparison over a regex that did not
match evaluates true, and a count-of-violations rule reads a renamed upstream
key as zero violations — both silent false passes.rolegains a third value,
integrity, for the validator that proves the counted field was present.
validators/aws/alb_encryption_in_transit.yamlis reshaped accordingly; in its
previous form it returned PASS on drifted evidence where 1 of 4 load balancers
was encrypted. -
The
suggest-validatorskill writes validators. It now authors them into
validators/rather than only proposing a regex, and verifies them against compliant,
non-compliant, and unreadable evidence.
Deprecated
- The inline
validatorsblock onfetcher.yaml. Nothing in the framework reads
it and onlygitlab/significant_change_notificationsstill carries one; it stays
in the schema, marked deprecated, so that fetcher's data remains shape-checked
until it migrates to the registry. New validators belong invalidators/.
Removed
- The docs the illustrated guides replaced.
docs/design.md,
authoring_a_fetcher.md,config_injection_design.md,envelope_design.md,
fetcher_contract.md,inventory_fetchers.md,issue_report_fetchers.md,
pipelines.md,platform_onboarding.md,private_mirror_workflow.md,
releasing.md,run_manifest_reference.md,uploader_design.md,
validators_design.mdandversioning.mdare gone. Links now point at the
guides (design.md's atarchitecture_guide.md); the onboarding source lists
keep pinned copies.
Fixed
-
Azure management calls go to the credential's cloud. Every
management-plane Azure fetcher sent its requests to commercial Azure, so a US
Government, Germany or China subscription failed withSubscriptionNotFound.
The management endpoint now followsAZURE_AUTHORITY_HOST, as the Entra
fetchers already did. -
An optional secret that cannot be resolved falls back to ambient identity.
The TUI wires optional secrets to default environment variables. When such a
variable was unset, the entry failed before it ran (exit 255), even with a
task role or workload identity present. -
Four run-path failures that exited 0. An unquoted YAML date in a target
stopped the run. The timeout now stops the whole process group, so a hung
awsorcurlcall cannot outlast it. Quitting the TUI during a run no
longer marks the running fetcher failed.guard_duty,guard_duty_findings
andglobal_accelerator_hanow record a service that is not enabled instead
of failing. -
checkov_biceppasses its list settings to checkov intact (0.2.0 →
0.2.1).checks,skip_checks,skip_pathsandskip_resourcestake
entries separated by commas or newlines, each trimmed.checksand
skip_checksalso take spaces and tabs, since check ids never contain them,
and_shared/skip-checks.default.txtis read the same way, with#
starting a comment anywhere on a line. Configured values reach checkov as
--flag=value. Before:- a
skip_pathsentry with a space (My Templates/legacy) became two skip
paths, and each piece could skip folders it was not meant to; - a
skip_pathsentry with*or?was expanded against the files in the
fetcher's working directory, so*.jsoncould reach checkov as
--skip-path report.json; checksfailed the run if it had a space after a comma
(CKV_AZURE_3, CKV_AZURE_44), ids separated only by spaces, or one id
per line;skip_checksfailed the run if its ids were separated only by spaces,
and read only its first line, so checks listed below it still ran;- a line of
_shared/skip-checks.default.txtholding several ids or a
trailing#comment failed the run; skip_resourcespatterns on separate lines became one pattern that
matched nothing;- a
skip_pathsorrepo_idvalue starting with-was read as an option,
and the run failed.
skip_pathsentries are Python regular expressions, which is how checkov
reads them: it searches each file's path for every entry, so.,*,?,
+,(,),[and]need a backslash to match literally. They are
not globs. An entry that is not a valid regular expression, such as
*.json, now fails the run withbad_confignaming it, before anything
is fetched; checkov's Bicep scan crashed on one. The check needspython3
on PATH, which the checkov category now declares.metadata.skip_pathsnow holds the entries checkov was given as
--skip-pathvalues. It used to disagree in three cases:- a path with a space was one entry, while checkov got two;
- a
*pattern was listed as written, while checkov got the files it
matched; - entries on separate lines with no comma between them were one entry,
while checkov got one per line.
New
metadata.checkslists thechecksallowlist, so a restricted scan no
longer reads as a full one. It is empty when every check ran. - a
-
azure_policy_assignmentsrecognises theAuditActioneffect (0.2.0 →
0.3.0). An assignment whose effect resolved toAuditAction, the audit
counterpart ofDenyAction, was reported asunresolvedwith no reason and
raisedunresolved_effect_assignments. It now resolves toauditAction, an
audit-only effect.mutateandaddToNetworkGroup(both in Microsoft's
effects reference) andenforceSetting(used by built-in definitions) now
resolve as enforcing effects. Every assignment'spolicy_effects.effect_counts
and the summary'smember_policy_effect_countstherefore gain four keys,
zero unless used:auditAction,mutate,addToNetworkGroupand
enforceSetting. Any other unrecognised value is still unresolved, but its
reasonnow names the value (unknown effect value 'X'). Two new summary
keys count unresolved member policies:unresolved_member_policies, and
unresolved_member_policies_not_enforcedfor those in any assignment whose
enforcementMode is not Default. Members of an initiative whose definition
can't be read aren't counted; that assignment shows only in
unresolved_effect_assignments. The per-assignment fields are unchanged: a
partly resolved DoNotEnforce initiative is still classed by its resolved
members, so itseffect_classcan readaudit_onlyand
enforcing_effects_not_enforcedfalse. The new summary keys are where that
gap shows. -
azure_entra_authentication_policycounts X.509 as phishing-resistant only
when it is multi-factor (0.1.0 → 0.2.0). An enabled X.509 certificate method
was always listed inphishing_resistant_methods_enabled, even with its default
authentication mode set tox509CertificateSingleFactor, which does not satisfy
MFA by itself. It now counts only when the default mode is
x509CertificateMultiFactor; a new summary field,
x509_certificate_multi_factor, reports the mode (nullwhen it is not
reported). -
azure_entra_mfa_statusfinds every admin (0.1.0 → 0.2.0). Admin status
came only from the registration report'sisAdminflag, which exists only on
users with a registration row, so an admin who had registered nothing was
counted as a non-admin andadmin_mfa_coverage_percentagewas overstated.
Admins are now the holders of privileged directory roles (the same list
azure_entra_privileged_rolesuses), directly or through a role-assignable
group, plus anyone the report flags. Each user carries
privileged_directory_rolesandis_admin_in_registration_report; the summary
addsadmin_users_without_mfa,admin_users_without_registration_detailsand
admin_role_membership_readable. The fetcher now readsGET /directoryRoles,
the members of each privileged role and the transitive members of any group
holding one: covered byDirectory.Read.All, which the Entra fetchers already
document (orRoleManagement.Read.Directory+GroupMember.Read.All). If that
read is denied the run fails and the admin figures readnull. -
Checkov evidence no longer overstates pass rates (
checkov_terraformand
checkov_kubernetes0.1.0 → 0.2.0).skip_resourcesdeleted failed checks on
matching resources, so they lefttotal_checksand raised
aggregate_percentage; they are now moved toskipped_checks(marked
SKIPPED, with the matching pattern insuppress_comment) and counted in
summary.skipped_by_resource_filter. An empty pattern from a trailing comma
no longer matches every resource. The 17 checks in
skip-checks-k8s.default.txt, among themCKV_K8S_20andCKV_K8S_31, were
always skipped and left no trace in the evidence. The effective skip list is
now recorded inmetadata.skip_checks, and the newuse_default_skip_checks
knob (defaulttrue, so results are unchanged) set tofalselets
skip_checksreplace the defaults. When the requested branch could not be
cloned, the scan fell back to the default branch butmetadata.branchstill
named the requested one.metadata.branchnow names the branch actually
scanned, withrequested_branchandbranch_fallbackbeside it. -
gcp_cloud_sql_encryption_statusreads point-in-time recovery per engine
(0.1.0 → 0.1.1). The fetcher took the first non-null of
pointInTimeRecoveryEnabledandbinaryLogEnabled, so a MySQL instance with
the binary log on (MySQL's PITR switch) and the generic toggle atfalse
read as having no PITR. MySQL now readsbinaryLogEnabledand PostgreSQL /
SQL Server readpointInTimeRecoveryEnabled, the same rule
gcp_cloud_sql_backup_configurationuses. Each record adds the two raw flags
(pitr_toggle_enabled,binary_log_enabled) and the summary adds
point_in_time_recovery_instances. -
gcp_compute_instance_configurationcounts OS Login 2FA only where OS
Login is on (0.1.0 → 0.1.1).enable-oslogin-2fadoes nothing without
enable-oslogin, butos_login_2fa_enabled(per instance and per project)
and theos_login_2fa_instances/project_os_login_2fa_enabledsummary
fields reported the flag alone. They now report effective 2FA (OS Login on
and the 2FA flag on, each resolved instance-first). The raw flag is kept as
os_login_2fa_configured. -
gcp_gke_cluster_configurationrecognises Binary Authorization set by
evaluation mode (0.1.0 → 0.1.1).binary_authorization_enabledread only
the deprecatedenabledflag, so a cluster enforcing with
PROJECT_SINGLETON_POLICY_ENFORCEread as off and was left out of
binary_authorization_clusters. Any mode other thanDISABLEDnow counts as
enabled. The legacy flag decides only when no mode is set, since the API
ignores it otherwise. It is kept asbinary_authorization_legacy_enabled. -
Every uploader reads "already exists" / "already connected" the same way.
The API refuses a repeat evidence-set create or a repeat CONNECT instead of
ignoring it, and each uploader held its own test for that refusal: the scripts
uploader treated a repeat CONNECT as success, the validator syncer failed on
it, and the evidence-set fallbacks recognised the refusal only as an HTTP 400.
API spec 0.9.2 permits the same refusal as a 409 Conflict on every operation,
which the 400-only tests would have missed. One shared rule
(framework/paramify_responses.py) now accepts 400 or 409 when the message
names the duplicate, and nothing else, so re-running any stage stays
idempotent on either code. The validator syncer also resolves its token like
the other uploaders (PARAMIFY_UPLOAD_API_TOKEN, falling back to
PARAMIFY_API_TOKEN) and reports a failed evidence-set lookup as its own
error rather than a bare HTTP exception. -
aws_security_groupsshows all-traffic and IPv6 rules (0.2.0 → 0.3.0).
A rule with no port range — protocol-1, "All traffic" — was dropped from
the evidence, so an inbound all-traffic rule from0.0.0.0/0and every
default allow-all egress rule were invisible. Such rules are now kept, with
FromPortandToPortset tonull. IPv6 ranges were never read, so a rule
open only to::/0looked closed; each rule now carriesIPv6CIDRs(IPv6
ranges joined with", ") afterCIDRs. Existing keys and their order are
unchanged. -
aws_iam_mfa_statusreports FIDO security keys asfido(0.1.0 →
0.2.0). Every device whose serial is an ARN was typedvirtual, so a FIDO/U2F
security key (arn:aws:iam::<account>:u2f/...), the phishing-resistant
factor, read as an authenticator app.Typeis nowfidofor a:u2f/
serial,virtualfor a:mfa/serial,hardwarefor a bare serial (a
hardware TOTP token) andunknownfor any other shape. -
aws_iam_policieslists permissions-boundary use apart from attachments
(0.2.0 → 0.3.0).AttachedUsersandAttachedRolesalso named the users and
roles that use the policy only as their permissions boundary, which grants
nothing. They now hold attachments only, and two new keys after
AttachedRoles,BoundaryUsersandBoundaryRoles, name the users and roles
that use it as their boundary. Existing keys and their order are unchanged. -
aws_security_groupsshows rules that reference security groups and
prefix lists (0.3.0 → 0.4.0). Only IP ranges were read, so a rule whose
source (outbound: destination) is another security group or a managed prefix
list came out with empty CIDRs and no source, as if it allowed nothing. Each
rule now ends withSecurityGroupIdsandPrefixListIds, joined with", "
likeCIDRs; a group owned by another account is written
<account-id>/<group-id>. Each group record also gainsGroupNameand
VpcIdafterRules. Keys are only appended. -
aws_network_firewall_rulescounts Suricata and domain-list rules
(0.1.0 → 0.2.0).StatefulRuleCountread onlyStatefulRules, so a stateful
rule group written as a Suricata rules string or a domain list reported 0.
A rules string now counts its lines that are not blank or#comments
(backslash-continued lines count once); a domain list counts its domains
(Targets), not the rules AWS generates per target type. A new
StatefulRuleSourcekey names which of the three was counted. -
aws_auto_scaling_high_availabilitylists each group's instances. A
query error leftInstancesempty for every Auto Scaling group. -
aws_organizations_scpkeeps the organization record when one SCP cannot
be read. A single unparseable policy used to drop the whole organization
from the evidence. -
AWS failure reports name the fetcher instead of
aws__shared, and an
expiredaws loginsession is reported as expired credentials.