Skip to content

chore(security): address the OSS pre-release audit ahead of the jamf org transfer - #74

Merged
Neil Martin (neilmartin83) merged 4 commits into
mainfrom
chore/oss-audit-pre-transfer
Sep 14, 2026
Merged

Neil Martin (neilmartin83) merged 4 commits into
mainfrom
chore/oss-audit-pre-transfer

Conversation

@neilmartin83

@neilmartin83 Neil Martin (neilmartin83) commented Sep 12, 2026 •

Copy link
Copy Markdown
Member

Closes the open items from a §9.2.4 Developer Pre-Release Checklist run against main on 2026-09-12, ahead of the AppSec request to transfer this repository into the jamf GitHub org. terraform-provider-jamfplatform went 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.md and docs/WIRE-FACTS.md is 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.md added

OSS 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_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 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 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/, since Logger itself is generated and types.go must not be edited.

The guarantee alongside it is scoped to the half that holds. LogRequest is passed no headers and the OAuth2 token exchange runs on its own http.Client outside the logged path, so neither the bearer token nor the client credential reaches it. LogResponse is a different matter: it receives the response http.Header unfiltered, which is the stated reason the acceptance tracer prints headers from a fixed allowlist rather than filtering them — see docs/STYLE.md and acc_helpers_test.go. The godoc and the README now say both halves, and tell callers to filter in LogResponse, 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 / jssUrl spellings, because Jamf names each of them in a published spec this repository already ships. Nothing under api/ was touched, per the house rule.

Three of the affected files are generated, so the fix went into tools/generate/emit.go and config.go rather than into jamfplatform/permissions.go, proclassic/xml_helpers.go and its test. make generate is at a fixpoint.

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 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.md accumulates 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_group fixture 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, on example.invalid where an address is needed, and the fixture named after a person is renamed directoryAccountFixture. Three files carried that one through tools/generate/emit.go, so the template was fixed and the tree regenerated; make generate is 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.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 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 --plan and a make generate drift check: all clean.

🤖 Generated with Claude Code

…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>
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>
@neilmartin83
Neil Martin (neilmartin83) merged commit 05d4e22 into main Sep 14, 2026
20 checks passed
@neilmartin83
Neil Martin (neilmartin83) deleted the chore/oss-audit-pre-transfer branch September 14, 2026 12:24

This branch was successfully deployed

1 active deployment
acceptance — f25cee20 Deployed Sep 12, 2026 by neilmartin83 via Acceptance (pro) #369
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants