chore(security): address the OSS pre-release audit ahead of the jamf org transfer - #74
Merged
Merged
Conversation
…ransfer Brings this repository to the same standard terraform-provider-jamfplatform reached for ASKAS-447, ahead of the equivalent AppSec request to transfer this repository into the jamf GitHub org. The 2026-03-16 §9.2.4 checklist run predates 646 of the 673 commits now in history, and two of its three follow-up items were never closed. SECURITY.md added Required by OSS Policy §9.1.6, and absent since the repository was published. Copied verbatim from the ESOS "SECURITY.md file template" that AppSec substituted into the provider under ASKAS-447 — the same file the provider carries today. Nothing hand-written: the provider's first attempt was written to Engineering Guidance §2.5(a)-(d) and AppSec replaced it wholesale, including striking a 5-business-day acknowledgement the in-force policy does not state. Starting from the template skips that round trip. .gitleaks.toml added — a full-history scan was reporting 17 findings All 17 are example values, and every one is traceable to something upstream or generated rather than authored here: a base64 string in api/pro_api.json that decodes to "This is not a token. Hopefully it looks like a token, but it's not.", an `apiClientId` documentation UUID, two `verificationKey` placeholders in api/account_sso_api.json, an `AppAndBookTokenId` UUID the generator copies out of the spec's `example` into a godoc comment, a trace-ID literal in a unit test, and one expired Auth0 example JWT whose every claim is example.com. There is no credential in this history. The suppressions match on the SECRET, anchored and fully escaped, not on fingerprints — and that choice is the provider's lesson rather than a preference. A gitleaks fingerprint is `<introducing-commit>:<file>:<rule>:<line>`, and both varying halves are unstable here: api/*.json is regenerated from upstream specs on every bundle ingest, so lines move and new introducing commits appear for text that has not changed. The provider's .gitleaksignore was pinned to a commit a rebase had removed; both entries silently stopped matching, and CI could not see it because gitleaks-action scans pushed commits rather than full history. Only the full-history scan — what AppSec actually runs — exposed it. Verified narrow: gitleaks reports the identical findings on a canary tree with and without the config, so it suppresses nothing beyond the seven literals it names. Full history is now clean at 599 commits, and TruffleHog reports 0 verified / 0 unverified under the prescribed command. README: Getting help and Troubleshooting The two items the 2026-03-16 audit left open. Troubleshooting is written against the refusals a caller actually hits — the 401 that means "no policy for this API product" as often as "bad credential", the token exchange that 404s on a base URL carrying a path prefix, OWNERSHIP_FORBIDDEN for a crossed-over scope, and the repeated BAD_PERMISSIONS that is a missing gateway route rather than a missing grant. WithLogger: say that nothing is redacted The SDK logs nothing unless a Logger is installed, so it has none of the provider's FINDING-1 exposure itself — but it hands a consumer the raw request body, and that body carries ClientSecret, AdminPassword, KeystorePassword and the escaped plist in a configuration profile's Payloads. The interface's contract has to say so, or a naive implementation leaks exactly what the provider's did. Recorded on WithLogger in client.go, the one handwritten file in jamfplatform/; Logger itself is generated and types.go must not be edited. Also states the two things a Logger cannot see, so the warning does not overclaim: LogRequest is passed no headers, so the bearer never reaches it, and the OAuth2 token exchange runs on its own http.Client outside the logged path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tracked tree named five non-public Jamf repositories 129 times across 22 files — the spec source, the gateway's authorization policy, the service that deploys its bundle, the gateway API definitions and its plugins — along with individual policy filenames, 30 internal pull-request numbers, internal commit SHAs, eight internal Jira keys and a non-production hostname. AppSec has filed that class of finding before, on the grounds that internal repository structure is itself sensitive, and this repository is about to be transferred into the jamf org. Rather than argue the point in the ticket, it is gone. What went, and what deliberately did not Removed: the five repository names in every form, the `*.rego` policy filenames, the internal directory layout (`policies/tyk_external/<domain>/`, the authz plugin's own source paths), the OPA bundle publishing and rollout mechanics, one verbatim rego rule quoted as a code block, every internal PR number and commit SHA, the eight Jira keys, and the stage hostname. Kept: everything the wire establishes, which is the whole point of these documents. The 1475/1454 registry-versus-allowlist agreement, the account privilege pipeline coupling and its three-way corroboration, the two-layer device-groups diagnosis, the validation orderings, the rollout-lag floors — all still here, now attributed to "the gateway's authorization policy" and "an upstream spec change" rather than to a repository a reader cannot open anyway. Dates survive everywhere a commit SHA was dropped, which is what makes the change findable again by someone who does have access. Also kept: Skyway, Wandera and the `JSSResource` / `jssUrl` spellings. Each is named by Jamf in a published spec this repository already carries, so redacting them here would be theatre. Nothing under `api/` was touched at all — those are upstream's published specs, and the house rule forbids editing them. The provenance is not lost `docs/local/internal-provenance.local.md` holds all 129 captured lines with their file and line number, a glossary of what each internal source is, and a crosswalk from every new public phrasing back to its internal referent. A future session can still answer "which internal change explains this behaviour" and go read it. `docs/local/` is now gitignored, so the file cannot be committed by accident. Two test-facing phrasings needed care rather than deletion `skywayBlockPinned` and `prestagePut500` put a Jira key in the failure message so a red lane names the standing fault it is pinning. Deleting the key would leave "a known defect" with no handle, so both now use a stable descriptive name — "the standing distributor-service fault", "the prestage PUT-500 defect" — which the crosswalk maps back. The guard conditions are untouched. Three of the affected files are generated, so the fix is in the generator `jamfplatform/permissions.go`, `jamfplatform/proclassic/xml_helpers.go` and its test carry these comments from `tools/generate/emit.go`. Edited there and regenerated; `make generate` is at a fixpoint and the generated diff is exactly those three files. Verified: `go build`, `go vet`, `go vet -tags acceptance`, the full unit suite, `gofmt`, `copywrite headers --plan` and a `make generate` drift check all clean, and a full-history gitleaks scan still reports no leaks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A stop-slop pass over the three user-facing surfaces the previous two commits touched. No behaviour changes and no claims changed; the gitleaks scan still reports no leaks and the config still suppresses only its seven named literals. What changed, and why each pattern had to go: - **Passive voice throughout.** "The SDK logs nothing unless a Logger is installed" hides who installs it. It now reads "until you install a Logger", which also puts the reader in the seat where the decision sits. Same for "nothing is redacted for you", "LogRequest is passed no headers" and a dozen others. - **Em dashes.** Gone from all three files. The README and client.go still carry plenty in their older prose, so these sections now read slightly differently from their neighbours. That is a deliberate trade rather than an oversight: matching the house style here would mean reintroducing the pattern. - **Binary contrasts.** "alternatives, not aliases" told the reader what the two options are not before saying what they are. The troubleshooting entry now states the mechanism directly: the two options stamp different headers, and a client carries one scope. - **"X that Y" headings.** "A token exchange that 404s" became "The token exchange answers 404". - **Negation in a heading.** `.gitleaks.toml`'s "WHY VALUE-BASED ALLOWLISTS AND NOT .gitleaksignore FINGERPRINTS" became "WHY THE ALLOWLISTS MATCH ON VALUE", and the paragraph under it now names an actor: someone pinned the provider's entries to a commit a later rebase removed, rather than the entries having "silently stopped matching" on their own. - **Adverbs and lazy extremes.** "silently", "constantly", "commonly", "every secret a write sends" (now "whatever secrets a write sends"), "Every one is" (now "All seven are"). - **One sentence that was simply wrong** and survived because it sounded plausible: `.gitleaks.toml` claimed "`make permmap`-style generated trees mean the api/*.json files are rewritten on every spec ingest". `make permmap` refreshes the permissions-map snapshot and has nothing to do with `api/`. The paragraph below already makes the real point, so the sentence is deleted rather than corrected. The three remaining em dashes in lines these commits touched sit in pre-existing sentences where only the citation changed. Rewriting those would mean editing prose this work has no reason to touch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 18:19 — with
GitHub Actions
Active
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 18:19 — with
GitHub Actions
Active
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 18:19 — with
GitHub Actions
Active
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 18:19 — with
GitHub Actions
Active
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 18:19 — with
GitHub Actions
Active
Neil Martin (neilmartin83)
enabled auto-merge
September 12, 2026 18:23
A PR review of this branch found that the redaction pass had the wrong pattern set. It matched internal repository names, pull-request numbers, Jira keys, commit SHAs and the stage hostname — and therefore missed every real tenant, environment, credential, device and person the wire probes had been recorded against. Those are the same class of finding for the same reason, so this closes them, and corrects a guarantee the audit's own WithLogger warning overstated. Personal data. jamfplatform/classicgroups_typed_test.go carried a full name and three email addresses captured from a real tenant response, asserted by value in three tests; the generator template carried a fourth in a fixture named after the person, and two more turned up in an LDAP server name and a profile fixture. All replaced with example.invalid placeholders, the fixture renamed to directoryAccountFixture, and the comment claiming the list fixture came from a real tenant corrected. datajar was acquired by Jamf, so these are colleagues' work addresses rather than third-party personal data, but a published SDK's test fixtures have no reason to name anyone. Identifiers. Seven real organization, environment and tenant UUIDs, a Security Cloud tenant, a UEM connector id, two probe-created group ids, eight gateway trace ids, four tenant and environment names, a production jamfcloud.com host named after an engineer, two internal service hostnames and one internal commit SHA the first pass missed. Every one is now a placeholder that keeps the surrounding claim readable: the counts and dates carried the evidence, never the identifiers. Test-device serials, UDIDs and MACs went too, for consistency rather than because they were judged sensitive. WithLogger claimed categorically that a consumer's Logger never sees the bearer token, justified only via LogRequest carrying no headers. But LogResponse receives the response http.Header unfiltered, and this repo asserts in two other places — docs/STYLE.md and acc_helpers_test.go — that Authorization reaches it in full, which is why the acceptance tracer prints headers from a fixed allowlist. A guarantee that turns out to be conditional is worse than none, because it is the reason a consumer skips filtering. The godoc and the README now scope the guarantee to LogRequest and tell callers to filter in LogResponse. Verified: go build, go vet, go vet -tags acceptance, the full unit suite, the generator's own tests, gofmt, copywrite headers --plan and make generate at a fixpoint. The provenance crosswalk in the gitignored docs/local/ records every original value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 19:53 — with
GitHub Actions
Active
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 19:53 — with
GitHub Actions
Active
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 19:53 — with
GitHub Actions
Active
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 19:53 — with
GitHub Actions
Active
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 19:53 — with
GitHub Actions
Active
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 19:57 — with
GitHub Actions
Active
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 19:57 — with
GitHub Actions
Active
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 19:57 — with
GitHub Actions
Active
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 19:57 — with
GitHub Actions
Active
Neil Martin (neilmartin83)
had a problem deploying
to
acceptance
September 12, 2026 19:57 — with
GitHub Actions
Failure
Neil Martin (neilmartin83)
force-pushed
the
chore/oss-audit-pre-transfer
branch
from
September 12, 2026 20:06
8ba74a6 to
f25cee2
Compare
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 20:07 — with
GitHub Actions
Active
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 20:07 — with
GitHub Actions
Active
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 20:07 — with
GitHub Actions
Active
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 20:07 — with
GitHub Actions
Active
Neil Martin (neilmartin83)
deployed
to
acceptance
September 12, 2026 20:07 — with
GitHub Actions
Active
Graham Pugh (grahampugh)
approved these changes
Sep 14, 2026
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the open items from a §9.2.4 Developer Pre-Release Checklist run against
mainon 2026-09-12, ahead of the AppSec request to transfer this repository into thejamfGitHub org.terraform-provider-jamfplatformwent through the equivalent under ASKAS-447 and moved on 9 or 10 September.The previous checklist run for this repository dates from 2026-03-16 and predates 646 of the 673 commits now in history. Two of its three follow-ups had never been closed.
Mostly this is a tidy-up. The engineering content of
CLAUDE.mdanddocs/WIRE-FACTS.mdis unchanged — both exist to record what the wire does, and every wire fact, figure and date in them still stands. What changes is the incidental detail those records had accumulated: citations to repositories a reader outside Jamf cannot open, and the specific tenants, hosts and fixtures the probes happened to run against. None of it was load-bearing.SECURITY.mdaddedOSS Policy §9.1.6 requires it and the repository had none since publication. This is the ESOS template verbatim, the one AppSec substituted into the provider under ASKAS-447. Nothing hand-written: the provider's first attempt was written to Engineering Guidance §2.5(a)-(d) and AppSec replaced it wholesale, including striking a 5-business-day acknowledgement the in-force policy does not state. Starting from the template skips that round trip.
README: Getting help and Troubleshooting
The two items the 2026-03-16 audit left open. Troubleshooting covers the refusals a caller actually hits: the 401 that means "no policy for this API product" as often as "bad credential", the token exchange that 404s on a base URL carrying a path prefix,
OWNERSHIP_FORBIDDENfor a crossed-over scope, and the repeatedBAD_PERMISSIONSthat is a missing gateway route rather than a missing grant.WithLogger: say that nothing is redactedThe SDK logs nothing until a consumer installs a
Logger, so it carries none of the provider's FINDING-1 exposure itself. It does hand over the raw request body, and that body carriesClientSecret,AdminPassword,KeystorePasswordand the escaped plist in a configuration profile'sPayloads. The interface's contract has to say so, or a naive implementation leaks exactly what the provider's did. Recorded onWithLoggerinclient.go, the one handwritten file injamfplatform/, sinceLoggeritself is generated andtypes.gomust not be edited.The guarantee alongside it is scoped to the half that holds.
LogRequestis passed no headers and the OAuth2 token exchange runs on its ownhttp.Clientoutside the logged path, so neither the bearer token nor the client credential reaches it.LogResponseis a different matter: it receives the responsehttp.Headerunfiltered, which is the stated reason the acceptance tracer prints headers from a fixed allowlist rather than filtering them — seedocs/STYLE.mdandacc_helpers_test.go. The godoc and the README now say both halves, and tell callers to filter inLogResponse, because a guarantee that turns out to be conditional is the reason someone skips filtering.Non-public Jamf repositories no longer named
The tracked tree named five non-public Jamf repositories 129 times across 22 files: the spec source, the gateway's authorization policy, the service that deploys its bundle, the gateway API definitions and its plugins. It also carried individual policy filenames, the internal directory layout, one verbatim policy rule quoted as a code block, 30 internal pull-request numbers, internal commit SHAs, eight internal Jira keys and a non-production hostname. A reader outside Jamf can open none of it, and AppSec has filed that class of finding before on the grounds that internal repository structure is itself sensitive.
Kept: everything the wire establishes, which is why these documents exist. The registry-versus-allowlist agreement figures, the account privilege pipeline coupling, the two-layer device-groups diagnosis, the validation orderings. A description replaced each citation and every date stayed, which is what keeps the change findable by someone who holds access.
Also kept: Skyway, Wandera and the
JSSResource/jssUrlspellings, because Jamf names each of them in a published spec this repository already ships. Nothing underapi/was touched, per the house rule.Three of the affected files are generated, so the fix went into
tools/generate/emit.goandconfig.gorather than intojamfplatform/permissions.go,proclassic/xml_helpers.goand its test.make generateis at a fixpoint.Two test-facing phrasings needed care rather than deletion.
skywayBlockPinnedandprestagePut500put a Jira key in the failure message so a red lane names the standing fault it pins; both now use a stable descriptive name instead. Guard conditions untouched.Fixtures and probe records sanitised
The pass above was keyed on repository and ticket citations, which left the other half of the same tidy-up: the tenants, hosts, devices and people the records happened to name.
docs/WIRE-FACTS.mdaccumulates the most of it, because every entry in it is a probe against a real tenant.Replaced with placeholders: seven organization, environment and tenant UUIDs, a Security Cloud tenant, a UEM connector id, two probe-created group ids, eight gateway trace ids, four tenant and environment names, a Jamf Pro host named after an engineer, two internal service hostnames and one internal commit SHA the first pass missed. Two of those hostnames sat inside a bullet that documents them as a leak.
Nothing is lost by it, because the counts and the dates always carried the evidence and the identifiers never did:
<org-a>holds 16 licences and 5 domains whether or not its UUID is printed, and<trace-1>through<trace-3>show three distinct requests exactly as well as the real ids did.Test fixtures got the same treatment. A
user_groupfixture captured from a real tenant response named a colleague and three addresses at datajar, since acquired by Jamf; a directory-account fixture was named after another; an LDAP server, a configuration-profile fixture, two device serials, two UDIDs and a MAC address were all real. All synthetic now, onexample.invalidwhere an address is needed, and the fixture named after a person is renameddirectoryAccountFixture. Three files carried that one throughtools/generate/emit.go, so the template was fixed and the tree regenerated;make generateis a fixpoint again. The comment claiming a list fixture came from a real tenant response is corrected, because it no longer does.A gitignored local file keeps the map back: all 129 original citations with file and line number, a glossary of each internal source, and a table from every placeholder to the value it replaced.
Secrets
A full-history scan turns up 17 findings over 7 distinct values, and every one is an example rather than a credential: a base64 string in
api/pro_api.jsonthat decodes to "This is not a token. Hopefully it looks like a token, but it's not.", anapiClientIddocumentation UUID, twoverificationKeyplaceholders inapi/account_sso_api.json, anAppAndBookTokenIdUUID the generator copies out of the spec'sexampleinto a godoc comment, a trace-ID literal in a unit test, and one expired Auth0 example JWT whose claims are all documentation placeholders. Five of the seven are in published specs this repository mirrors verbatim and must not edit.No suppression file ships with that adjudication. GitHub native secret scanning is the org standard and cannot read one, so it would have been inert after the transfer, and writing one means copying seven secret-shaped literals into a second tracked file — worth avoiding in a repository about to be public, harmless though these particular values are. The write-up goes to the reviewers who need it instead.
Secret scanning, push protection, AI detection, non-provider patterns and validity checks are all enabled here and report no open alerts. TruffleHog reports 0 verified findings.
Verification
go build,go vet,go vet -tags acceptance, the full unit suite, the generator's own tests,gofmt,copywrite headers --planand amake generatedrift check: all clean.🤖 Generated with Claude Code