Summary
Be\Framework\SemanticLog\Logger::openChain() records every public property of the Input object into the becoming_open context's prop payload via ObjectPropertyExtractor (get_object_vars() + ReflectionClass). Nothing filters it, so a credential carried as a public promoted property — the normal shape of a Be Input — lands in the semantic log in plaintext.
#[\SensitiveParameter] on those constructor parameters does not help: it only redacts PHP stack traces, not a property dump.
Reproduction (BeMart, 1.x)
BeMart wires bear/event-sourcing's SemanticLogInvoker (which redacts request params by default) and Be Framework's becoming logger into the same observation log. Driving one admin login through the observe context:
php bin/observe.php post '/admin/login?loginId=test-admin&password=local-dev-admin-password&csrfToken=...'
php .claude/skills/bear-observe/harness/tree.php
renders:
└── resource_request uri=page://self/admin/login method=POST params={"loginId":"test-admin","password":"[FILTERED]",...} replayable=false → resource_response code=200
└── becoming_open input=MyVendor\BeMart\Be\Input\AdminLoginInput prop={"loginId":"test-admin","password":"local-dev-admin-password"} → becoming_close exit=success ...
The resource layer redacted the password to [FILTERED]; one level down, becoming_open.prop carries the same value verbatim. grep -c local-dev-admin-password var/log/.../observe/latest.json → 1, and it is in the becoming_open context only.
The same applies to every credential-shaped public promoted property in a Be Input: ResetPasswordInput::$resetKey/$password, SetTwoFactorAuthInput::$authKey, ActivateCustomerInput::$secretKey, ChangeAdminPasswordInput::$currentPassword/$changePasswordFirst/$changePasswordSecond. BeMart already annotates all of these with #[\SensitiveParameter].
Where
src/SemanticLog/Logger.php — openChain() builds BecomingOpenContext(prop: $this->extractProperties($input)). BeingCloseContext / BeingFinalCloseContext carry a prop the same way.
src/SemanticLog/ObjectPropertyExtractor.php — extract() returns every storable public property; no redaction hook.
Proposal
Have ObjectPropertyExtractor honor #[\SensitiveParameter]: it already reflects the class, so for each promoted property check whether the matching constructor parameter carries the attribute and, if so, store a fixed sentinel (e.g. [FILTERED]) instead of the value. Same for the BeingClose / BeingFinalClose prop payloads, since a Final/Being can re-expose the same value as a public property (BeMart's TwoFactorAuthConfigured::$authKey does).
This reuses an attribute applications already apply for stack-trace safety, so an Input that is correctly annotated is protected in the semantic log with no extra configuration. Reusing the attribute is preferable to a name-based filter here because the Input is the authoritative declaration of what each field means — the application has already said which ones are secrets.
Related
Summary
Be\Framework\SemanticLog\Logger::openChain()records every public property of the Input object into thebecoming_opencontext'sproppayload viaObjectPropertyExtractor(get_object_vars()+ReflectionClass). Nothing filters it, so a credential carried as a public promoted property — the normal shape of a Be Input — lands in the semantic log in plaintext.#[\SensitiveParameter]on those constructor parameters does not help: it only redacts PHP stack traces, not a property dump.Reproduction (BeMart,
1.x)BeMart wires
bear/event-sourcing'sSemanticLogInvoker(which redacts request params by default) and Be Framework's becoming logger into the same observation log. Driving one admin login through the observe context:php bin/observe.php post '/admin/login?loginId=test-admin&password=local-dev-admin-password&csrfToken=...' php .claude/skills/bear-observe/harness/tree.phprenders:
The resource layer redacted the password to
[FILTERED]; one level down,becoming_open.propcarries the same value verbatim.grep -c local-dev-admin-password var/log/.../observe/latest.json→1, and it is in thebecoming_opencontext only.The same applies to every credential-shaped public promoted property in a Be Input:
ResetPasswordInput::$resetKey/$password,SetTwoFactorAuthInput::$authKey,ActivateCustomerInput::$secretKey,ChangeAdminPasswordInput::$currentPassword/$changePasswordFirst/$changePasswordSecond. BeMart already annotates all of these with#[\SensitiveParameter].Where
src/SemanticLog/Logger.php—openChain()buildsBecomingOpenContext(prop: $this->extractProperties($input)).BeingCloseContext/BeingFinalCloseContextcarry apropthe same way.src/SemanticLog/ObjectPropertyExtractor.php—extract()returns every storable public property; no redaction hook.Proposal
Have
ObjectPropertyExtractorhonor#[\SensitiveParameter]: it already reflects the class, so for each promoted property check whether the matching constructor parameter carries the attribute and, if so, store a fixed sentinel (e.g.[FILTERED]) instead of the value. Same for theBeingClose/BeingFinalCloseproppayloads, since a Final/Being can re-expose the same value as a public property (BeMart'sTwoFactorAuthConfigured::$authKeydoes).This reuses an attribute applications already apply for stack-trace safety, so an Input that is correctly annotated is protected in the semantic log with no extra configuration. Reusing the attribute is preferable to a name-based filter here because the Input is the authoritative declaration of what each field means — the application has already said which ones are secrets.
Related
[FILTERED]in the first line above). Its README explicitly scopes itself toresource_requestand notes that "a separate observation pathway (e.g. an application's own domain-level logger) is a different boundary and needs its own redaction" — this is that pathway.