Add setTargets script for bulk assessment targets - #11
Open
JaredEzz wants to merge 4 commits into
Open
Conversation
setTargets reads every aspect an API key can reach into a CSV with its
current target, and writes filled-in targets and weights back through
PUT /v1/businessUnits/{id}/assessments/{id}/targets.
Before saving it checks the aspect ids against the assessment, rejects
duplicates, and reports who already holds targets there, since an
aggregate business unit averages targets across managers rather than
replacing them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The deployed API gates the public save-targets route on the Manager role and its 403 names that role. Quoting a permission string described a state that is not live, and the closed-assessment rejection comes back worded for both scores and targets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The targets endpoint saves a request as a unit, so a batch that holds a rejected row must not be sent at all. A duplicate or unparseable row now drops its whole assessment rather than quietly saving the rows around it. Other ways a run could mislead or strand someone: - The confirmation prompt reads the same stream as the API key, so a piped key made it unanswerable partway through a run. The key comes from OSTRICH_API_KEY or a hidden prompt, and an unattended run without --yes is refused before the first write. - --template refused nothing when overwriting, so a filled-in template could be lost. It now needs --overwrite, and takes a path. - A template written from a partial read exited zero while claiming to hold every aspect. Unreadable assessments are listed and exit non-zero, and business units the key holds nothing on are counted. - The read-back check matched a target from one contributor against a weight from another. Both now have to come from the same one, and the wording no longer claims the value is attributed to the caller. - Requests had no timeout, --baseUrl accepted plaintext, and the key was echoed while being typed. - A header carrying neither target nor weight reported nothing to do. - Template files are written with a BOM so Excel round-trips them as UTF-8, and a non-UTF-8 file gets an instruction rather than a stack trace. flatten_business_units builds display paths on the way down instead of storing parents on the payload, which keeps the API response free of the reference cycles that made it untestable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
JaredEzz
marked this pull request as ready for review
August 4, 2026 21:54
Both survived earlier review because the mock assumed a response shape
the real API doesn't return.
- save_targets() crashed with KeyError: 'response' on every real,
successful save. The endpoint only ever returns {"message": ...}, no
"response" envelope; _request() now takes has_response_body and
save_targets() opts out of the envelope it never gets. Confirmed
live: the write always landed server-side, only the client crashed
on the success response.
- confirm_shared_targets() caught EOFError and returned False, so an
unattended run that couldn't answer the prompt printed "treating as
no" and exited 0, having saved nothing, indistinguishable from a
deliberate skip. It now raises SetupError so main() reports exit 2
instead. --dryRun no longer reaches the prompt at all: it states who
already holds targets without asking, since a dry run should never
block on an answer it doesn't need.
Verified against the real dev API, not the mock: the exact save that
used to crash now completes and reads back correctly, a
duplicate-aspect batch still writes nothing, and the EOF path fails
loudly instead of silently.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
JaredEzz
force-pushed
the
CU-868kervpp_Python-script-for-mass-setting-of-targets
branch
from
August 5, 2026 21:24
4ad5780 to
5dbceda
Compare
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.
https://app.clickup.com/t/868kervpp
new
setTargets/script for setting assessment targets in bulk, for customers who don't want to click through every control one at a time.--template [CSV]writes every aspect the key can reach with its current target. won't clobber an existing file without--overwrite, so a half-filled template survives a re-run--applygroups the filled-in rows by assessment and sends onePUT /v1/businessUnits/{businessUnitId}/assessments/{assessmentId}/targetsper assessmentaspectId, unknownaspectId, unparseable target, bad weight) drops that whole assessment instead of writing the rows around it/scoresafter each write and requires target and weight to come from the same contributor before calling a value presentOSTRICH_API_KEYor a hidden prompt. unattended--applywithout--yesis refused up front rather than hitting an unanswerable prompt mid-run--dryRunvalidates and prints without writing. exit status is non-zero for any rejected row, skipped assessment, failure, or value that didn't read backsetTargets/README.mdcovers key creation, the Manager role requirement, csv format, the Excel save-as gotcha, and aggregate vs overrideexercised against a stubbed api across 11 cases, then run live against a real dev tenant, which caught two real bugs the mock hid (see the collapsible below). both are fixed and reverified live. one live-only finding is a backend defect, not a bug in this script, also below.
AC
--template, confirm one row per aspect and that rollup assessments are absent--template, confirm it refuses; then--overwrite, confirm it replaces--apply --dryRun, confirm nothing is written--dryRun, confirm the targets show in the app and every value reads backaspectIdtwice in one assessment, confirm that assessment writes nothing (not just the second row)aspectIdin the csv, confirm that assessment is skipped and no request is sentOSTRICH_API_KEY=... python setTargets.py --apply f.csvwith no tty and no--yes, confirm it refuses before writing anything--applyatset targets example.csvwith the ids swapped for real ones, confirm the minimal 5-column form workstwo bugs the mock hid, found and fixed by testing live against dev
the mock in the earlier review faked responses. testing against the real dev api (
apid.ostrichcybersecurity.com) with a real Manager-scoped key surfaced two real bugs the mock's wrong assumptions had hidden, both fixed and reverified against the real api:save_targets()crashed withKeyError: 'response'on every real, successful save. the targets endpoint only ever returns{"message": ...}, noresponseenvelope, unlike every read endpoint. confirmed live: the write always landed correctly server-side, only the client crashed reading its own success. fixed by making the envelope check opt-out per call instead of universal.EOFErrorand returnedFalse, so it printed "treating as no" and exited 0 having saved nothing, indistinguishable from a deliberate skip. now raises and exits non-zero. found this becausesys.stdin.isatty()reportsTrueunder< /dev/nullredirection on this platform (windows git bash + native python), so the early refusal never fired and the run reached the actual prompt.--dryRunno longer reaches the prompt at all now either, since a dry run should never block on an answer it doesn't need.one open, known limitation from the same pass: if one assessment already saved before a later one in the same run hits this exact eof case, the run exits without printing the usual saved-count summary. the values already saved are correct and unaffected, only the summary line is missing. not fixed here, flagged for a follow-up.
found live, not a bug in this script: businessUnits list 500s for a nested-bu-scoped key
GET /v1/businessUnits/(the first call--templatemakes) 500'd against the real dev api for a key scoped to a business unit that isn't top-level in firestore, reproduced with a raw curl outside the script entirely. every other endpoint (assessments, content, scores, the targets put itself) resolved the same nested bu correctly via its path parameter. a key scoped to a subsidiary or department rather than the company root is a completely normal way to scope a customer key, so this blocks--templatefor exactly that case.this is a backend defect, not something in this PR's diff, and out of scope to fix here. worth its own ticket against
emu.notes: the readme describes roles, on purpose
the readme states the requirement as the Manager role on the business unit and does not go below that. that is what the deployed api checks and what its 403 names, and it stays accurate as role handling changes, so the readme should not need rewriting later.
the closed-assessment row quotes
Cannot save scores or targets on a closed assessmentfor the same reason. that is the message the shared scores-and-targets handler returns, ahead of the targets-only mapping further down.why the aspect list comes from two calls
/scoresreturns one entry per function, category, question and aspect. thenodesmap in/contentholds every function, category and question, so removing those keys and thesummaryrow leaves the aspects, which are the only ids that accept a target.matching a pattern against the ids would also work for the built-in frameworks, where an aspect id like
GV.OC-1-PROCESShas one more hyphen segment than the questionGV.OC-1. custom content types don't have to follow that shape, so the script derives the set instead.aggregate vs override, and what the read-back check can and cannot prove
targets are attributed to the account that created the key, not to the key itself. on an aggregate business unit that means a bulk load replaces that user's own targets and leaves every other manager's in the average, so the same csv gives a different result depending on who runs it. the script reads the existing contributors out of
targetLabelsand the per-subtargetvalues, names them, and asks before writing.the read-back check afterwards confirms a requested value is present on the assessment, not that it is attributed to the caller. the public api doesn't expose the calling account's own
subId, so per-user verification isn't available; requiring target and weight to match one contributor closes the worst version of it, where one manager's target paired with another's weight read as a match. the readme says this in as many words rather than claiming more than it checks.switching the strategy afterwards is not neutral either. aggregate to override carries the averaged values over, but only on assessments that have never held override targets before, otherwise the older override values resurface. override to aggregate carries nothing over.
notes
ostrichApi/is vendored per folder, matchingexportAssessmentStatus/andcreateScenarioFamilies/. no shared package.utf-8-sigeither way, and a non-utf-8 file gets a save-as instruction rather than a stack trace.75.0are rejected rather than coerced. guessing at a malformed value is worse than naming the row.targetandweightblank are skipped, so a partly filled template is a valid input.--baseUrlhas to be https, since the key and the token both cross it.requestsis pinned at2.34.2.README.mdnow indexes the three script folders, since customers land there from a shared link.