1. Recovering data includes recovering controls
A fictional support team restores a fund-position table before reporting distribution. Row count matches the source and the administrator’s query finishes without error. These results demonstrate only parts of recovery. The team still needs to establish whether each group sees authorized portfolios and whether protected identifiers remain unavailable to people who should not read them. This scenario does not describe internal BNP Paribas procedures. Before the operation, inventory four dependencies: data, access policies, executing identities and material required for recovery. Record relevant copies, exported results and historical versions too. For each dependency, define the evidence to collect after the change. A privileged-identity check can confirm availability while hiding a segregation failure. Include a representative identity for each scope and an access attempt that should be refused. The PM’s responsibility is to ensure these checks, owners and a clear decision point are in the plan. APS observes service behavior, the data owner confirms permitted scope and the technical team demonstrates effective configuration. If functional recovery finishes first, keep outstanding protection checks visible. Reporting urgency can justify an approved, limited operational alternative; it does not turn a correct count into authorization evidence. At shift handover, always distinguish “data restored,” “access checked” and “service accepted.” Explain which observation supports each statement so the incoming team can continue without repeating assumptions.
2. Review the operation replacing the table
Policies on a copied BigQuery table become independent of the source. If source access is restricted tomorrow, do not assume the copy follows that change. Identify copies that remain available and the owner maintaining controls on each one. Copy time helps locate initial state but does not prove current policy. Review should compare configuration on the resource the consumer actually queries. The overwrite operation matters too. Copying a source without row access policies over a protected destination can remove destination policies; documentation distinguishes the append case. TRUNCATE TABLE retains schema and policies, whereas operations overwriting the table with WRITE_TRUNCATE do not provide the same preservation. Similar names are insufficient reason to consider two runbooks equivalent. Analyze the semantics of the operation that will actually execute. For reporting, draw the whole sequence: restrict use during recovery, execute the approved operation, confirm data, restore or review required controls, and test access. The administrator’s query is only one check. Add a query by a team with limited scope and another attempting data outside that scope. Retain configuration revision and results without including unnecessary sensitive values in the ticket. If the destination must remain suspended, communicate the missing evidence and next decision time. The acceptance record should make both a successful allowed query and an effective restriction understandable to someone who did not run them.
3. Follow identity, caching and history
An export uses the executing identity’s permissions. The analyst requesting a file might have masked-only access while the job account has Fine-Grained Reader and obtains original values. Record the effective principal and inspect produced content. Destination bucket authorization controls who opens the file, but does not demonstrate that a column was masked during export. These checks complement each other. When a copy job fails for a masked-only identity, granting full column access changes the data boundary. Review the need and an approved alternative for producing the limited result. Do not judge the fix solely by the disappearance of an error. The team must confirm that delivered output respects agreed scope. Use distinct synthetic values so the difference between original and masked output is visible without exposing real information. After restricting access, consider earlier cached results and historical data. Documentation describes cached-result reuse after replacing Fine-Grained Reader with Masked Reader. A check without cache helps observe current enforcement without erasing copies already obtained. If a column was cleaned while retaining schema, historical values can remain accessible within the time-travel window under documented conditions. Plan when to relax protection with that window in mind. Neither cleaning current data nor changing one permission proves that every earlier representation disappeared. List the evidence needed for each remaining copy instead of treating one successful permission change as universal cleanup.
4. Coordinate secret versions without losing changes
A deployment creates secret version 18 and needs to confirm that payload. Access by number identifies the intended version and has the strong-consistency guarantee documented for add then access. That specific guarantee does not automatically extend to latest or aliases. Use a reference appropriate to what you are checking and retain the version in deployment evidence. Do not record the secret value in the ticket or a diagnostic log. Authorization has a separate timing dimension: IAM changes can propagate eventually. Do not invent a universal revocation duration or confuse version access with permission propagation. For an access change, identify who no longer needs access, which resources are covered and how the outcome will be observed. The check should use the intended identity; the operator account does not replace that context. ETags help prevent a runbook from imposing an old intention over a concurrent change. If another team modified the version and the condition fails, read current state, compare changes and decide again. Removing the ETag for automatic retry removes precisely the protection that revealed the conflict. Distinguish a secret’s ETag from its version’s ETag: they belong to different resources. In an international shift handover, include change intent and observed state without credentials. The receiving engineer can then decide whether to continue, correct or abandon the pending operation rather than blindly retrying it.
5. Retiring keys requires dependency knowledge
Rotating a KMS key creates a new version but does not automatically re-encrypt existing archives. If an archive depends on v3 and the primary becomes v4, a successful new write does not establish old-data recovery. Direct re-encryption still needs usable previous material to decrypt before encrypting with the new version. Define the sequence and recovery evidence before retiring dependencies. Usage inventory helps investigation but has limits. It tracks supported CMEK usage rather than every direct application use of a key. It can be limited to the queried project and aggregate object usage to a bucket. An empty list, partial list or aggregate entry does not demonstrate absence of dependencies. Complement it with application owners, version records and recovery checks appropriate to the archive. Keep uncertainty explicit in the decision when evidence remains incomplete. For asymmetric signing, coordinate distribution of the new public key and consumer changes. Creating material does not update external verifiers. Consider pending messages that still require the earlier verification version too. If a required version was accidentally disabled and recovery is authorized, re-enabling it can resolve that dependency while material exists; confirm the service afterward. Command acceptance proves neither successful startup nor recovery of every consumer. The plan should distinguish availability, protection and permanent material removal, and assign an owner to evidence for each step before the next irreversible decision.
6. Interpret AI inspection by dimension
A fictional application summarizes synthetic tickets to support an APS team. Policy requires particular filters and a transformation before forwarding content. In a Model Armor response, invocation success and absence of a finding are separate dimensions. invocationResult=SUCCESS does not cancel MATCH_FOUND. PARTIAL indicates skipped or failed filters. For an individual filter, EXECUTION_SKIPPED does not establish completed inspection. Read results and details relevant to policy instead of converting HTTP success into authorization. Transformation is another dimension. Receiving deidentified text does not automatically change the original string retained by the application. Check which payload is actually forwarded and which request produced it. A check showing only text displayed to the user might not observe the application-to-model path. Keep that verification in integration testing with synthetic data that makes a version mix-up visible. Also confirm capabilities of the selected integration. The direct API and a connector do not necessarily provide identical behavior; the documented Gemini Enterprise integration blocks SDP findings rather than deidentifying. The inspected template exclusions are in Preview. They evaluate raw text without automatic translation, so they need checks in the languages used. To investigate NO_MATCH_FOUND with exclusions, retain configuration and inspect diagnostic labels in sanitize logs when enabled. An aggregate state does not by itself explain how the decision was reached. Capture enough context to distinguish a change in policy from a change in input.
7. Exercise: select the matching content
This lesson’s local program receives a synthetic payload, tenant, request, prompt or response phase, template revision and validity window. Declared evidence includes the same identifiers, an input hash and normalized results for required filters. These fields belong to the exercise; they are neither a client nor a complete reproduction of the Model Armor API. Evidence authenticity is not verified. Run python3 run.py and compare cases. The favorable scenario selects the transformed text’s hash, which differs from the original. Changing the payload after inspection creates a mismatch. Changing tenant, request, phase or revision also prevents the local policy from being satisfied. The program retains reasons and presents no selected payload when gaps exist. A hash compares bytes; it is not encryption, anonymization or authorization evidence. Predict the outcome before changing one variable. A skipped mandatory filter remains missing even when transformation is complete. An unknown result is not allow. Evidence from minute 90 is inside the window when now=100 and maxAge=10; evidence from minute 89 is stale. If required transformation came from different content, its source hash reveals the difference. Also explain the case where an optional filter is ignored by the local model: the program shows its name but does not decide whether the author’s chosen requirement list was adequate. That judgment needs review outside this worksheet.
8. Hand useful evidence to the next team
Complete the exercise with an English decision note: identify the operation, observed result, limitation and owner of the next action. Reporting can lack a destination policy despite correct counts. Key retirement can lack confirmation from a consumer outside the visible project. AI processing can have valid transformation and an unexecuted mandatory filter. The note should expose these combinations instead of reducing everything to one green indicator. Define the next check concretely. “Validate security” does not identify the identity, data version or path to observe. Prefer stating the limited query that should work, the out-of-scope query that should fail, the archive requiring recovery or the synthetic payload that should reach the forwarding point. Record the revision used so a later change does not automatically inherit earlier evidence. The code accesses no cloud services, reads no real secrets, performs no inspections and does not approve production. It demonstrates consistency between supplied declarations. An attacker able to fabricate all evidence could make it internally consistent; comparing hashes does not authenticate its producer. That limitation belongs in the debrief. At work, trustworthy evidence origin, authorization and integration need verification through their respective mechanisms. Learning is complete when you can explain both the worksheet’s decision and the additional information needed for an operational decision. Keep these limitations alongside the result when handing the investigation to the next team.
"""Original offline policy worksheet; not a Model Armor client or security control."""
import copy
import hashlib
import itertools
import json
import re
from pathlib import Path
def digest(text):
return hashlib.sha256(text.encode('utf-8')).hexdigest
def require(condition, message):
if not condition:
raise ValueError(message)
def text(value):
return isinstance(value, str) and bool(value.strip)
def integer(value):
return type(value) is int and value >= 0
def sha(value):
return isinstance(value, str) and re.fullmatch('[0-9a-f]{64}', value) is not None
def assess(data):
require(isinstance(data, dict), 'input object')
require(all(text(data.get(k)) for k in ('tenant', 'requestId', 'templateRevision', 'payload')), 'context')
require(data.get('phase') in ('prompt', 'response'), 'phase')
require(integer(data.get('now')) and integer(data.get('maxAge')), 'integer minutes')
require(type(data.get('requireTransformation')) is bool, 'transformation flag')
required = data.get('requiredFilters')
require(isinstance(required, list) and required and all(text(x) for x in required), 'required filters')
require(len(set(required)) == len(required), 'unique filters')
e = data.get('evidence')
require(isinstance(e, dict), 'evidence object')
require(all(text(e.get(k)) for k in ('tenant', 'requestId', 'templateRevision')), 'evidence context')
require(e.get('phase') in ('prompt', 'response'), 'evidence phase')
require(integer(e.get('checkedAt')) and e['checkedAt'] <= data['now'], 'evidence time')
require(sha(e.get('inputSha256')), 'input digest')
filters = e.get('filters')
require(isinstance(filters, dict), 'filter map')
for name, value in filters.items:
require(text(name) and isinstance(value, dict), 'filter record')
require(value.get('execution') in ('success', 'skipped', 'error'), 'execution')
require(value.get('decision') in ('allow', 'block', 'unknown'), 'decision')
transformation = e.get('transformation')
if transformation is not None:
require(isinstance(transformation, dict), 'transformation object')
require(transformation.get('status') in ('complete', 'failed'), 'transformation status')
require(sha(transformation.get('inputSha256')), 'transformation input digest')
require(text(transformation.get('text')), 'transformed text')
reasons = []
for field in ('tenant', 'requestId', 'templateRevision', 'phase'):
if data[field]!= e[field]: reasons.append('mismatch:' + field)
input_sha = digest(data['payload'])
if e['inputSha256']!= input_sha: reasons.append('mismatch:payload')
if e['checkedAt'] < data['now'] - data['maxAge']: reasons.append('stale-evidence')
for name in sorted(required):
f = filters.get(name)
if f is None: reasons.append('missing-filter:' + name)
elif f['execution']!= 'success': reasons.append('incomplete-filter:' + name)
elif f['decision']!= 'allow': reasons.append('nonpermitting-filter:' + name)
output = data['payload']; selection = 'original'
if data['requireTransformation']:
selection = 'transformed'
if transformation is None: reasons.append('missing-transformation')
else:
if transformation['status']!= 'complete': reasons.append('failed-transformation')
if transformation['inputSha256']!= input_sha: reasons.append('mismatch:transformation-input')
output = transformation['text']
ready = not reasons
return {'readyUnderDeclaredPolicy': ready, 'reasons': reasons,
'selectedPayload': selection if ready else None,
'selectedSha256': digest(output) if ready else None,
'ignoredFilterNames': sorted(set(filters) - set(required)),
'liveInspectionPerformed': False, 'evidenceAuthenticityVerified': False,
'actualAuthorizationEvaluated': False, 'productionUseApproved': False}
def sample:
payload='SYNTHETIC ticket client=FICT-42: batch late'
context=dict(tenant='training-a',requestId='req-17',templateRevision='t4',phase='prompt')
return dict(context,now=100,maxAge=10,payload=payload,requireTransformation=True,
requiredFilters=['injection','uri'],
evidence=dict(context,checkedAt=95,inputSha256=digest(payload),
filters={n:dict(execution='success',decision='allow') for n in ['injection','uri']},
transformation=dict(status='complete',inputSha256=digest(payload),text='SYNTHETIC ticket client=[REDACTED]: batch late')))
def checks:
fixtures=[]
def check(name,data,ready,reason=None):
before=copy.deepcopy(data);actual=assess(data);assert data==before
assert actual['readyUnderDeclaredPolicy']==ready,name
if reason:assert reason in actual['reasons'],name
if not ready:assert actual['selectedSha256'] is None and actual['selectedPayload'] is None
fixtures.append(dict(id=name,**actual));return actual
x=sample;a=check('transformed-selected',x,True)
assert a['selectedSha256']==digest(x['evidence']['transformation']['text'])!=digest(x['payload'])
x=sample;x['requireTransformation']=False;check('original-permitted-by-local-policy',x,True)
for field,value in [('tenant','training-b'),('requestId','req-18'),('templateRevision','t5'),('phase','response')]:
x=sample;x['evidence'][field]=value;check('wrong-'+field,x,False,'mismatch:'+field)
x=sample;x['payload']+=' changed'check('changed-payload',x,False,'mismatch:payload')
x=sample;x['evidence']['checkedAt']=89;check('stale',x,False,'stale-evidence')
x=sample;x['evidence']['checkedAt']=90;check('window-boundary',x,True)
x=sample;x['evidence']['filters']['injection']['execution']='skipped'check('skipped-filter',x,False,'incomplete-filter:injection')
x=sample;del x['evidence']['filters']['uri'];check('missing-filter',x,False,'missing-filter:uri')
x=sample;x['evidence']['filters']['uri']['decision']='block'check('blocked-filter',x,False,'nonpermitting-filter:uri')
x=sample;x['evidence']['filters']['uri']['decision']='unknown'check('unknown-filter',x,False,'nonpermitting-filter:uri')
x=sample;x['evidence']['transformation']=None;check('missing-transformation',x,False,'missing-transformation')
x=sample;x['evidence']['transformation']['status']='failed'check('failed-transformation',x,False,'failed-transformation')
x=sample;x['evidence']['transformation']['inputSha256']=digest('different');check('wrong-transformation-input',x,False,'mismatch:transformation-input')
x=sample;x['evidence']['filters']['optional']=dict(execution='error',decision='unknown')
assert check('optional-outside-declared-policy',x,True)['ignoredFilterNames']==['optional']
combinations=0
for execution,decision,transform,revision in itertools.product(('success','skipped','error'),('allow','block','unknown'),('complete','failed'),('t4','t3')):
x=sample;x['evidence']['filters']['injection'].update(execution=execution,decision=decision)
x['evidence']['transformation']['status']=transform;x['evidence']['templateRevision']=revision
expected=execution=='success' and decision=='allow' and transform=='complete' and revision=='t4'
assert assess(x)['readyUnderDeclaredPolicy']==expected;combinations+=1
x=sample;x['requiredFilters'].append('safety');x['evidence']['filters']['safety']=dict(execution='success',decision='allow');ref=assess(x);permutations=0
for order in itertools.permutations(x['requiredFilters']):
y=copy.deepcopy(x);y['requiredFilters']=list(order);y['evidence']['filters']={n:x['evidence']['filters'][n]for n in order};assert assess(y)==ref;permutations+=1
invalid=[]
for k,v in [('now',True),('maxAge',-1),('payload',''),('phase','request'),('requireTransformation','yes'),('requiredFilters',[]),('requiredFilters',['x','x']),('evidence',None)]:
x=sample;x[k]=v;invalid.append(x)
for k,v in [('checkedAt',101),('checkedAt',1.5),('inputSha256','bad'),('tenant',''),('filters',[])]:
x=sample;x['evidence'][k]=v;invalid.append(x)
for k,v in [('execution','pass'),('decision','success')]:
x=sample;x['evidence']['filters']['injection'][k]=v;invalid.append(x)
for k,v in [('status','ready'),('inputSha256',''),('text',None)]:
x=sample;x['evidence']['transformation'][k]=v;invalid.append(x)
x=sample;del x['evidence']['inputSha256'];invalid.append(x)
x=sample;x['requiredFilters']=[''];invalid.append(x)
for x in invalid:
try:assess(x)
except ValueError:pass
else:raise AssertionError('invalid input accepted')
return {'fixtures':fixtures,'stateCombinations':combinations,'inputPermutations':permutations,'invalidInputs':len(invalid),
'inputPreserved':True,'orderIndependent':True,'network':False,'cloudExecuted':False,'persistentWrites':False,
'scriptSha256':hashlib.sha256(Path(__file__).read_bytes).hexdigest}
if __name__=='__main__':
print(json.dumps(checks,ensure_ascii=False,indent=2))
Recovered reporting has correct counts but lost row policies; an assistant has transformed text but forwards the original with a skipped filter. Both require checking the control that the favorable result does not establish.
Common pitfalls
Confusing counts with protection,rotation with re-encryption,partial inventory with no dependencies or HTTP success with complete inspection.
Related topics: Recovery and resilience · Identity and authorization · AI application security
Recovery should preserve usable data and appropriate controls; confirm the resource,identity,version and evidence for each decision.
Reference: Using row-level security with other BigQuery features · Current linked guide; edition date unconfirmed (2026-09-30 inspection)