diff --git a/AGENTS.md b/AGENTS.md index 5a0018c..8eb414c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,6 +67,13 @@ cargo check --workspace cargo test -q ``` +Regenerate committed JSON report snapshots after intentional report-output +changes: + +```bash +SESSIONSCOPE_UPDATE_JSON_SNAPSHOTS=1 cargo test -p sessionscope-testing --test json_snapshots +``` + For broader local validation: ```bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 7549172..c5a52ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,15 @@ and SessionScope uses semantic versioning as described in - Added v0.2 P1 cookie hardening coverage for `__Host-` / `__Secure-` prefix rules, `Partitioned` cookie review, broad non-session Domain review, and same-handler conflicting cookie writes across existing JS/TS/Python cookie detectors. - Added v0.2 P2 JWT crypto-trust coverage for `alg:none`, HMAC/asymmetric algorithm-confusion signals, `jku`/`x5u`/embedded-JWK header trust review, missing `nbf` validation, broad clock-skew review, and unvalidated `kid` header review across existing `jsonwebtoken`, `jose`, and PyJWT detector surfaces. - Added v0.2 P3 OAuth/OIDC flow integrity coverage for PKCE, `state`, OIDC `nonce`, broad redirect URI review, and client storage hygiene checks for localStorage, sessionStorage, URL path/fragment token exposure, and browser-path client secrets. +- Added `jwt_denylist_absent_on_logout_review` lifecycle coverage for access-JWT logout flows that lack linked denylist, blocklist, or token revocation-store evidence. +- Added `refresh_family_revocation_absent_on_logout_review` lifecycle coverage for refresh-token logout flows that lack linked family/user-scoped revocation evidence. +- Added `sliding_expiry_without_rotation_review` lifecycle coverage for rolling/sliding session expiry that lacks linked session or refresh-token rotation evidence. +- Added `password_change_global_revocation_absent_review` lifecycle coverage for password-change handlers that lack linked global session invalidation, refresh-family revocation, or token-version bump evidence. +- Added clean-baseline false-positive fixtures across Express, Next.js, FastAPI, Django, generic JS/TS, and generic Python to guard every v0.2 P1-P4 check ID. +- Added hand-rolled JSON report snapshot tests for representative Express, Next.js, FastAPI, Django, generic JS, generic TS, and generic Python fixtures. +- Added Rust integration-test coverage for the documented CLI advisory/enforce exit-code policy matrix. +- Documented the consolidated v0.2 category audit decision: all P1-P4 checks map to existing finding categories, with no schema or SARIF rule change. +- Completed the v0.2 edge-case hardening documentation pass across the changelog, README status, roadmap, and coverage matrix. - Extended report redaction for OAuth/OIDC `state`, `nonce`, `code_verifier`, and `code_challenge` values in assignments, object keys, and URL parameters. ### Pre-release remediation (v0.1.0 readiness) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4005454..02bcba7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,6 +39,12 @@ cargo check --workspace cargo test --workspace --all-targets ``` +Regenerate committed JSON report snapshots after intentional output changes: + +```bash +SESSIONSCOPE_UPDATE_JSON_SNAPSHOTS=1 cargo test -p sessionscope-testing --test json_snapshots +``` + Run the CLI during development: ```bash diff --git a/README.md b/README.md index 9d71350..fa4c785 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,8 @@ Config precedence is: ## Project status - **Release target:** v0.1.0 first packaged release through GitHub Releases. Release packaging, versioning, and installation workflow are tracked in #28. -- **Complete:** v0.1 foundation, v0.2 cookie audit, v0.3 JWT validation, v0.4 lifecycle mapping, v0.5 expanded token handling, v0.6 framework/provider coverage, v0.7 CI SARIF/enforcement, v0.8 reviewer workflows. +- **Complete:** v0.1 foundation plus the v0.2 depth-first edge-case hardening round for cookie rules, JWT crypto-trust, OAuth/OIDC flow integrity, client storage hygiene, lifecycle gaps, false-positive fixtures, JSON snapshots, and CLI exit-code tests. +- **Deferred:** v0.3+ breadth expansion for new languages and frameworks; see [docs/ROADMAP.md](docs/ROADMAP.md). - **Schema:** JSON contract v0.5.0. - **Rust:** edition 2024. MSRV is 1.95. - **Platforms:** Linux, macOS, and Windows are covered by CI where workflow support exists. diff --git a/crates/sessionscope-classifier/src/lifecycle.rs b/crates/sessionscope-classifier/src/lifecycle.rs index e92e98d..d38cd63 100644 --- a/crates/sessionscope-classifier/src/lifecycle.rs +++ b/crates/sessionscope-classifier/src/lifecycle.rs @@ -25,6 +25,7 @@ pub fn link(report: &ScanReport) -> Vec { pub fn classify(report: &ScanReport) -> Vec { let mut findings = Vec::new(); + let evidence_by_id = evidence_by_id(report); for path in &report.lifecycle_paths { let Some(artifact) = artifact_for_path(report, path) else { @@ -32,7 +33,6 @@ pub fn classify(report: &ScanReport) -> Vec { }; findings.extend(classify_issue_without_validate(artifact, path)); - let evidence_by_id = evidence_by_id(report); findings.extend(classify_refresh_without_revoke( artifact, path, @@ -50,6 +50,30 @@ pub fn classify(report: &ScanReport) -> Vec { path, &evidence_by_id, )); + findings.extend(classify_jwt_denylist_absent_on_logout( + artifact, + path, + report, + &evidence_by_id, + )); + findings.extend(classify_refresh_family_revocation_absent_on_logout( + artifact, + path, + report, + &evidence_by_id, + )); + findings.extend(classify_sliding_expiry_without_rotation( + artifact, + path, + report, + &evidence_by_id, + )); + findings.extend(classify_password_change_global_revocation_absent( + artifact, + path, + report, + &evidence_by_id, + )); } findings @@ -592,6 +616,188 @@ fn classify_cookie_clear_attribute_mismatch( )) } +fn classify_jwt_denylist_absent_on_logout( + artifact: &Artifact, + path: &LifecyclePath, + report: &ScanReport, + evidence_by_id: &BTreeMap<&str, &Evidence>, +) -> Option { + if artifact.artifact_type != ArtifactType::AccessJwt + || !(has_stage(path, LifecycleStage::Issue) || has_stage(path, LifecycleStage::Validate)) + { + return None; + } + + let logout_ids = linked_logout_handler_ids(path, report, evidence_by_id); + if logout_ids.is_empty() || has_linked_jwt_denylist_or_revoke(path, report, evidence_by_id) { + return None; + } + + let mut evidence_ids = logout_ids; + evidence_ids.extend(evidence_ids_for_stage(path, LifecycleStage::Issue)); + if evidence_ids.len() == 1 { + evidence_ids.extend(evidence_ids_for_stage(path, LifecycleStage::Validate)); + } + evidence_ids.sort(); + evidence_ids.dedup(); + + let name = artifact.display_name.as_deref().unwrap_or("access_jwt"); + Some(finding( + artifact, + path, + FindingSpec { + rule_id: "jwt_denylist_absent_on_logout_review", + category: FindingCategory::LifecycleGap, + severity: Severity::Medium, + evidence_ids, + title: format!("JWT `{name}` has logout evidence without linked denylist evidence"), + description: "A logout handler and access-JWT lifecycle evidence were detected in linked source context, but no source-bound denylist, blocklist, or token revocation-store insertion evidence was linked for the same logout flow." + .to_string(), + suggested_fix: + "Insert the JWT identifier into a denylist/blocklist or revoke-store on logout, or document the intentional short-TTL stateless model for reviewer confirmation." + .to_string(), + reviewer_question: format!( + "Where does logout revoke or denylist outstanding `{name}` tokens?" + ), + }, + )) +} + +fn classify_refresh_family_revocation_absent_on_logout( + artifact: &Artifact, + path: &LifecyclePath, + report: &ScanReport, + evidence_by_id: &BTreeMap<&str, &Evidence>, +) -> Option { + if !is_refresh_artifact(artifact) + || !(has_stage(path, LifecycleStage::Issue) + || has_stage(path, LifecycleStage::Store) + || has_stage(path, LifecycleStage::Refresh) + || has_stage(path, LifecycleStage::Validate)) + { + return None; + } + + let logout_ids = linked_logout_handler_ids(path, report, evidence_by_id); + if logout_ids.is_empty() || has_linked_refresh_family_revoke(path, report, evidence_by_id) { + return None; + } + + let mut evidence_ids = logout_ids; + evidence_ids.extend(evidence_ids_for_stage(path, LifecycleStage::Refresh)); + if evidence_ids.len() == 1 { + evidence_ids.extend(evidence_ids_for_stage(path, LifecycleStage::Store)); + } + if evidence_ids.len() == 1 { + evidence_ids.extend(evidence_ids_for_stage(path, LifecycleStage::Issue)); + } + evidence_ids.sort(); + evidence_ids.dedup(); + + let name = artifact.display_name.as_deref().unwrap_or("refresh_token"); + Some(finding( + artifact, + path, + FindingSpec { + rule_id: "refresh_family_revocation_absent_on_logout_review", + category: FindingCategory::LifecycleGap, + severity: Severity::Medium, + evidence_ids, + title: format!( + "Refresh token `{name}` has logout evidence without family revocation" + ), + description: "Logout and refresh-token lifecycle evidence were detected in linked source context, but no source-bound user-scoped or refresh-family revocation evidence was linked for the logout flow." + .to_string(), + suggested_fix: + "Revoke the user's refresh-token family, delete user-scoped refresh-token records, or remove the refresh-family cache key during logout." + .to_string(), + reviewer_question: format!( + "Where does logout revoke every refresh token in the `{name}` family or for the current user?" + ), + }, + )) +} + +fn classify_sliding_expiry_without_rotation( + artifact: &Artifact, + path: &LifecyclePath, + report: &ScanReport, + evidence_by_id: &BTreeMap<&str, &Evidence>, +) -> Option { + if !matches!( + artifact.artifact_type, + ArtifactType::SessionCookie + | ArtifactType::SignedCookie + | ArtifactType::SessionRecord + | ArtifactType::RefreshJwt + | ArtifactType::Unknown + ) || !has_sliding_expiry_evidence(path, evidence_by_id) + || has_linked_rotation_evidence(path, report, evidence_by_id) + { + return None; + } + + let evidence_ids = sliding_expiry_ids(path, evidence_by_id); + let name = artifact.display_name.as_deref().unwrap_or("session"); + Some(finding( + artifact, + path, + FindingSpec { + rule_id: "sliding_expiry_without_rotation_review", + category: FindingCategory::LifecycleGap, + severity: Severity::Low, + evidence_ids, + title: format!("Session `{name}` uses sliding expiry without linked rotation"), + description: "Sliding or rolling TTL/Max-Age evidence was detected, but no linked session regeneration, session-key cycling, refresh-token rotation, or reissue evidence was found for the same lifecycle path." + .to_string(), + suggested_fix: + "Pair sliding expiry with session or refresh-token rotation, or document the framework-managed rotation behavior for reviewer confirmation." + .to_string(), + reviewer_question: format!( + "Where is `{name}` rotated when its idle/sliding expiry is extended?" + ), + }, + )) +} + +fn classify_password_change_global_revocation_absent( + artifact: &Artifact, + path: &LifecyclePath, + report: &ScanReport, + evidence_by_id: &BTreeMap<&str, &Evidence>, +) -> Option { + let handler_ids = password_change_handler_ids(path, evidence_by_id); + if handler_ids.is_empty() + || has_linked_password_change_global_revoke(path, report, evidence_by_id) + { + return None; + } + + let mut evidence_ids = handler_ids; + evidence_ids.sort(); + evidence_ids.dedup(); + + Some(finding( + artifact, + path, + FindingSpec { + rule_id: "password_change_global_revocation_absent_review", + category: FindingCategory::LifecycleGap, + severity: Severity::Medium, + evidence_ids, + title: "Password-change handler lacks linked global session revocation".to_string(), + description: "A password-change handler was detected, but no linked global session invalidation, refresh-family revocation, or token-version bump evidence was found in the same source scope." + .to_string(), + suggested_fix: + "After password changes, revoke all active sessions/refresh-token families or bump a token/session version checked during authentication." + .to_string(), + reviewer_question: + "Where are existing sessions and refresh-token families invalidated after this password change?" + .to_string(), + }, + )) +} + fn attribute_missing_or_mismatched( observation: &sessionscope_model::CookieAttributeObservation, attribute: &str, @@ -778,105 +984,367 @@ fn has_dynamic_or_provider_refresh( }) } -fn client_cookie_clear_ids( +fn linked_logout_handler_ids( path: &LifecyclePath, + report: &ScanReport, evidence_by_id: &BTreeMap<&str, &Evidence>, ) -> Vec { - evidence_ids_for_stage(path, LifecycleStage::Revoke) + let direct_ids = evidence_ids_for_stage(path, LifecycleStage::Revoke) .into_iter() .filter(|evidence_id| { evidence_by_id .get(evidence_id.0.as_str()) - .is_some_and(|evidence| evidence.detector_id == "logout.cookie_clear") + .is_some_and(|evidence| evidence.detector_id == "logout.handler") }) + .collect::>(); + if !direct_ids.is_empty() { + return direct_ids; + } + + report + .evidence + .iter() + .filter(|evidence| evidence.detector_id == "logout.handler") + .filter(|evidence| evidence_linked_to_path_context(evidence, path, evidence_by_id)) + .map(|evidence| evidence.id.clone()) .collect() } -fn evidence_ids_for_stage(path: &LifecyclePath, stage: LifecycleStage) -> Vec { - path.stages +fn has_linked_jwt_denylist_or_revoke( + path: &LifecyclePath, + report: &ScanReport, + evidence_by_id: &BTreeMap<&str, &Evidence>, +) -> bool { + evidence_ids_for_stage(path, LifecycleStage::Revoke) .iter() - .find(|step| step.stage == stage) - .map(|step| step.evidence_ids.clone()) - .unwrap_or_default() + .any(|evidence_id| { + evidence_by_id + .get(evidence_id.0.as_str()) + .is_some_and(|evidence| is_jwt_denylist_or_revoke_evidence(evidence)) + }) + || report.evidence.iter().any(|evidence| { + is_jwt_denylist_or_revoke_evidence(evidence) + && evidence_linked_to_path_context(evidence, path, evidence_by_id) + }) } -fn fallback_path_ids(path: &LifecyclePath) -> Vec { - let mut ids = path - .stages +fn has_linked_refresh_family_revoke( + path: &LifecyclePath, + report: &ScanReport, + evidence_by_id: &BTreeMap<&str, &Evidence>, +) -> bool { + evidence_ids_for_stage(path, LifecycleStage::Revoke) .iter() - .flat_map(|step| step.evidence_ids.clone()) - .collect::>(); - ids.sort(); - ids.dedup(); - ids + .any(|evidence_id| { + evidence_by_id + .get(evidence_id.0.as_str()) + .is_some_and(|evidence| is_refresh_family_revoke_evidence(evidence)) + }) + || report.evidence.iter().any(|evidence| { + is_refresh_family_revoke_evidence(evidence) + && evidence_linked_to_path_context(evidence, path, evidence_by_id) + }) } -fn is_revoke_only_path(path: &LifecyclePath) -> bool { - path.stages.len() == 1 && has_stage(path, LifecycleStage::Revoke) +fn is_refresh_family_revoke_evidence(evidence: &Evidence) -> bool { + (evidence.detector_id == "refresh.reuse_detection" + && evidence.lifecycle_stage == LifecycleStage::Revoke) + || (matches!( + evidence.detector_id.as_str(), + "refresh.revoke" | "refresh.rotate" | "logout.token_revoke" | "logout.provider_revoke" + ) && evidence + .excerpt + .as_ref() + .is_some_and(|excerpt| contains_refresh_family_revoke_text(excerpt.as_str()))) } -fn is_query_param_transmit_path( +fn has_sliding_expiry_evidence( path: &LifecyclePath, evidence_by_id: &BTreeMap<&str, &Evidence>, ) -> bool { - path.stages.len() == 1 - && has_stage(path, LifecycleStage::Transmit) - && evidence_ids_for_stage(path, LifecycleStage::Transmit) - .iter() - .any(|evidence_id| { - evidence_by_id - .get(evidence_id.0.as_str()) - .is_some_and(|evidence| { - matches!( - evidence.detector_id.as_str(), - "query_param.read" | "query_param.read.dynamic" - ) - }) - }) + !sliding_expiry_ids(path, evidence_by_id).is_empty() } -fn artifact_has_only_revoke_evidence(artifact: &Artifact) -> bool { - !artifact.lifecycle_evidence.revoke.is_empty() - && artifact.lifecycle_evidence.issue.is_empty() - && artifact.lifecycle_evidence.store.is_empty() - && artifact.lifecycle_evidence.transmit.is_empty() - && artifact.lifecycle_evidence.validate.is_empty() - && artifact.lifecycle_evidence.refresh.is_empty() - && artifact.lifecycle_evidence.expire.is_empty() - && artifact.lifecycle_evidence.introspect.is_empty() +fn sliding_expiry_ids( + path: &LifecyclePath, + evidence_by_id: &BTreeMap<&str, &Evidence>, +) -> Vec { + fallback_path_ids(path) + .into_iter() + .filter(|evidence_id| { + evidence_by_id + .get(evidence_id.0.as_str()) + .is_some_and(|evidence| is_sliding_expiry_evidence(evidence)) + }) + .collect() } -fn compatible_revoke_artifacts(source: &Artifact, target: &Artifact) -> bool { - let names_match = normalized_artifact_name(source) == normalized_artifact_name(target); - let session_alias_match = - compatible_cookie_session_types(source.artifact_type, target.artifact_type) - && session_cookie_alias_matches(source, target); +fn is_sliding_expiry_evidence(evidence: &Evidence) -> bool { + matches!( + evidence.detector_id.as_str(), + "session.middleware" | "refresh.expire" | "refresh.store" + ) && evidence + .excerpt + .as_ref() + .is_some_and(|excerpt| contains_sliding_expiry_text(excerpt.as_str())) +} - if !names_match && !session_alias_match { - return false; - } - source.artifact_type == target.artifact_type - || source.artifact_type == ArtifactType::Unknown - || target.artifact_type == ArtifactType::Unknown - || compatible_cookie_session_types(source.artifact_type, target.artifact_type) - || compatible_token_types(source.artifact_type, target.artifact_type) +fn contains_sliding_expiry_text(value: &str) -> bool { + let normalized = value + .chars() + .filter(|ch| ch.is_ascii_alphanumeric() || *ch == '_') + .collect::() + .to_ascii_lowercase(); + (normalized.contains("rolling") + || normalized.contains("sliding") + || normalized.contains("idle") + || normalized.contains("touch") + || normalized.contains("refreshsessionttl") + || normalized.contains("extend_session") + || normalized.contains("extendsession")) + && (normalized.contains("maxage") + || normalized.contains("ttl") + || normalized.contains("expires") + || normalized.contains("expiresat") + || normalized.contains("expire")) } -fn revoke_paths_are_linkable( - source_artifact: &Artifact, - target_artifact: &Artifact, - source_path: &LifecyclePath, - target_path: &LifecyclePath, +fn has_linked_rotation_evidence( + path: &LifecyclePath, + report: &ScanReport, evidence_by_id: &BTreeMap<&str, &Evidence>, ) -> bool { - if !compatible_revoke_artifacts(source_artifact, target_artifact) { - return false; - } - - if (is_refresh_artifact(source_artifact) && is_refresh_artifact(target_artifact)) - || session_cookie_alias_matches(source_artifact, target_artifact) - { - return paths_have_linkable_source_context(source_path, target_path, evidence_by_id); + evidence_ids_for_stage(path, LifecycleStage::Refresh) + .iter() + .any(|evidence_id| { + evidence_by_id + .get(evidence_id.0.as_str()) + .is_some_and(|evidence| is_rotation_evidence(evidence)) + }) + || report.evidence.iter().any(|evidence| { + is_rotation_evidence(evidence) + && evidence_linked_to_path_context(evidence, path, evidence_by_id) + }) +} + +fn password_change_handler_ids( + path: &LifecyclePath, + evidence_by_id: &BTreeMap<&str, &Evidence>, +) -> Vec { + fallback_path_ids(path) + .into_iter() + .filter(|evidence_id| { + evidence_by_id + .get(evidence_id.0.as_str()) + .is_some_and(|evidence| evidence.detector_id == "password_change.handler") + }) + .collect() +} + +fn has_linked_password_change_global_revoke( + path: &LifecyclePath, + report: &ScanReport, + evidence_by_id: &BTreeMap<&str, &Evidence>, +) -> bool { + fallback_path_ids(path).iter().any(|evidence_id| { + evidence_by_id + .get(evidence_id.0.as_str()) + .is_some_and(|evidence| is_password_change_global_revoke_evidence(evidence)) + }) || report.evidence.iter().any(|evidence| { + is_password_change_global_revoke_evidence(evidence) + && evidence_linked_to_path_context(evidence, path, evidence_by_id) + }) +} + +fn is_password_change_global_revoke_evidence(evidence: &Evidence) -> bool { + evidence.detector_id == "password_change.global_revoke" + || is_refresh_family_revoke_evidence(evidence) +} + +fn is_rotation_evidence(evidence: &Evidence) -> bool { + matches!( + evidence.detector_id.as_str(), + "session.regenerate" + | "session.reissue" + | "session.framework_default_regenerate" + | "refresh.rotate" + ) +} + +fn contains_refresh_family_revoke_text(value: &str) -> bool { + let normalized = value + .chars() + .filter(|ch| ch.is_ascii_alphanumeric() || *ch == '_') + .collect::() + .to_ascii_lowercase(); + normalized.contains("refresh") + && (normalized.contains("family") + || normalized.contains("userid") + || normalized.contains("user_id") + || normalized.contains("usersessions") + || normalized.contains("allsessions") + || normalized.contains("allrefresh") + || normalized.contains("tokenfamily") + || normalized.contains("token_family") + || normalized.contains("familyid") + || normalized.contains("family_id")) + && (normalized.contains("revoke") + || normalized.contains("delete") + || normalized.contains("del") + || normalized.contains("invalidate") + || normalized.contains("destroy") + || normalized.contains("blacklist") + || normalized.contains("denylist")) +} + +fn is_jwt_denylist_or_revoke_evidence(evidence: &Evidence) -> bool { + evidence + .excerpt + .as_ref() + .is_some_and(|excerpt| contains_jwt_denylist_text(excerpt.as_str())) +} + +fn contains_jwt_denylist_text(value: &str) -> bool { + let normalized = value + .chars() + .filter(|ch| ch.is_ascii_alphanumeric() || *ch == '_') + .collect::() + .to_ascii_lowercase(); + let has_revoke_action = normalized.contains("denylist") + || normalized.contains("blocklist") + || normalized.contains("blacklist") + || normalized.contains("revokedtokens") + || normalized.contains("revokedtoken") + || normalized.contains("revoketoken") + || normalized.contains("addtoblocklist") + || normalized.contains("revoke"); + let has_access_token_context = normalized.contains("jwt") + || normalized.contains("jti") + || normalized.contains("access") + || (normalized.contains("token") && !normalized.contains("refresh")); + + has_revoke_action && has_access_token_context +} + +fn evidence_linked_to_path_context( + evidence: &Evidence, + path: &LifecyclePath, + evidence_by_id: &BTreeMap<&str, &Evidence>, +) -> bool { + path_evidence_locations(path, evidence_by_id) + .iter() + .any(|location| locations_are_linkable(&evidence.location, location)) +} + +fn locations_are_linkable(left: &SourceLocation, right: &SourceLocation) -> bool { + left.path == right.path + && left.line.is_some() + && right.line.is_some() + && left + .line + .zip(right.line) + .is_some_and(|(left, right)| left.abs_diff(right) <= REFRESH_LINK_MAX_LINE_DISTANCE) +} + +fn client_cookie_clear_ids( + path: &LifecyclePath, + evidence_by_id: &BTreeMap<&str, &Evidence>, +) -> Vec { + evidence_ids_for_stage(path, LifecycleStage::Revoke) + .into_iter() + .filter(|evidence_id| { + evidence_by_id + .get(evidence_id.0.as_str()) + .is_some_and(|evidence| evidence.detector_id == "logout.cookie_clear") + }) + .collect() +} + +fn evidence_ids_for_stage(path: &LifecyclePath, stage: LifecycleStage) -> Vec { + path.stages + .iter() + .find(|step| step.stage == stage) + .map(|step| step.evidence_ids.clone()) + .unwrap_or_default() +} + +fn fallback_path_ids(path: &LifecyclePath) -> Vec { + let mut ids = path + .stages + .iter() + .flat_map(|step| step.evidence_ids.clone()) + .collect::>(); + ids.sort(); + ids.dedup(); + ids +} + +fn is_revoke_only_path(path: &LifecyclePath) -> bool { + path.stages.len() == 1 && has_stage(path, LifecycleStage::Revoke) +} + +fn is_query_param_transmit_path( + path: &LifecyclePath, + evidence_by_id: &BTreeMap<&str, &Evidence>, +) -> bool { + path.stages.len() == 1 + && has_stage(path, LifecycleStage::Transmit) + && evidence_ids_for_stage(path, LifecycleStage::Transmit) + .iter() + .any(|evidence_id| { + evidence_by_id + .get(evidence_id.0.as_str()) + .is_some_and(|evidence| { + matches!( + evidence.detector_id.as_str(), + "query_param.read" | "query_param.read.dynamic" + ) + }) + }) +} + +fn artifact_has_only_revoke_evidence(artifact: &Artifact) -> bool { + !artifact.lifecycle_evidence.revoke.is_empty() + && artifact.lifecycle_evidence.issue.is_empty() + && artifact.lifecycle_evidence.store.is_empty() + && artifact.lifecycle_evidence.transmit.is_empty() + && artifact.lifecycle_evidence.validate.is_empty() + && artifact.lifecycle_evidence.refresh.is_empty() + && artifact.lifecycle_evidence.expire.is_empty() + && artifact.lifecycle_evidence.introspect.is_empty() +} + +fn compatible_revoke_artifacts(source: &Artifact, target: &Artifact) -> bool { + let names_match = normalized_artifact_name(source) == normalized_artifact_name(target); + let session_alias_match = + compatible_cookie_session_types(source.artifact_type, target.artifact_type) + && session_cookie_alias_matches(source, target); + + if !names_match && !session_alias_match { + return false; + } + source.artifact_type == target.artifact_type + || source.artifact_type == ArtifactType::Unknown + || target.artifact_type == ArtifactType::Unknown + || compatible_cookie_session_types(source.artifact_type, target.artifact_type) + || compatible_token_types(source.artifact_type, target.artifact_type) +} + +fn revoke_paths_are_linkable( + source_artifact: &Artifact, + target_artifact: &Artifact, + source_path: &LifecyclePath, + target_path: &LifecyclePath, + evidence_by_id: &BTreeMap<&str, &Evidence>, +) -> bool { + if !compatible_revoke_artifacts(source_artifact, target_artifact) { + return false; + } + + if (is_refresh_artifact(source_artifact) && is_refresh_artifact(target_artifact)) + || session_cookie_alias_matches(source_artifact, target_artifact) + { + return paths_have_linkable_source_context(source_path, target_path, evidence_by_id); } true @@ -1193,8 +1661,8 @@ fn artifact_type_part(artifact_type: ArtifactType) -> &'static str { #[cfg(test)] mod tests { use sessionscope_model::{ - ArtifactId, CookieAttributeObservation, LifecyclePathId, SCHEMA_VERSION, ScanSummary, - SourceLocation, + ArtifactId, CookieAttributeObservation, LifecyclePathId, SCHEMA_VERSION, SanitizedExcerpt, + ScanSummary, SourceLocation, }; use super::*; @@ -2168,6 +2636,465 @@ mod tests { assert!(finding.title.contains("dynamic refresh behavior")); } + #[test] + fn jwt_logout_without_denylist_is_lifecycle_gap() { + let mut report = report_with_artifacts( + vec![artifact( + "artifact_access", + ArtifactType::AccessJwt, + "access_token", + LifecycleEvidence { + issue: vec![EvidenceId("evidence_issue".to_string())], + ..LifecycleEvidence::default() + }, + )], + vec![ + evidence("evidence_issue", LifecycleStage::Issue, 10, false), + evidence_with_detector( + "evidence_logout", + LifecycleStage::Revoke, + "logout.handler", + 20, + false, + ), + ], + ); + report.lifecycle_paths = link(&report); + + let findings = classify(&report); + let finding = findings + .iter() + .find(|finding| finding.title.contains("without linked denylist")) + .expect("JWT logout denylist review finding"); + + assert_eq!(finding.category, FindingCategory::LifecycleGap); + assert_eq!(finding.severity, Severity::Medium); + assert!(finding.reviewer_question.is_some()); + assert!( + finding + .evidence_ids + .contains(&EvidenceId("evidence_issue".to_string())) + ); + assert!( + finding + .evidence_ids + .contains(&EvidenceId("evidence_logout".to_string())) + ); + } + + #[test] + fn linked_jwt_revoke_prevents_logout_denylist_gap() { + let mut report = report_with_artifacts( + vec![artifact( + "artifact_access", + ArtifactType::AccessJwt, + "access_token", + LifecycleEvidence { + issue: vec![EvidenceId("evidence_issue".to_string())], + ..LifecycleEvidence::default() + }, + )], + vec![ + evidence("evidence_issue", LifecycleStage::Issue, 10, false), + evidence_with_detector( + "evidence_logout", + LifecycleStage::Revoke, + "logout.handler", + 20, + false, + ), + evidence_with_detector( + "evidence_revoke", + LifecycleStage::Revoke, + "logout.token_revoke", + 21, + false, + ), + ], + ); + report.evidence[2].excerpt = Some(SanitizedExcerpt::from_sanitized( + "revokeAccessToken(accessToken)".to_string(), + )); + report.lifecycle_paths = link(&report); + + let findings = classify(&report); + + assert!( + !findings + .iter() + .any(|finding| finding.title.contains("without linked denylist")), + "{findings:?}" + ); + } + + #[test] + fn refresh_revoke_does_not_prevent_jwt_logout_denylist_gap() { + let mut report = report_with_artifacts( + vec![artifact( + "artifact_access", + ArtifactType::AccessJwt, + "access_token", + LifecycleEvidence { + issue: vec![EvidenceId("evidence_issue".to_string())], + ..LifecycleEvidence::default() + }, + )], + vec![ + evidence("evidence_issue", LifecycleStage::Issue, 10, false), + evidence_with_detector( + "evidence_logout", + LifecycleStage::Revoke, + "logout.handler", + 20, + false, + ), + evidence_with_detector( + "evidence_refresh_revoke", + LifecycleStage::Revoke, + "logout.token_revoke", + 21, + false, + ), + ], + ); + report.evidence[2].excerpt = Some(SanitizedExcerpt::from_sanitized( + "revokeRefreshToken(refreshToken)".to_string(), + )); + report.lifecycle_paths = link(&report); + + let findings = classify(&report); + + assert!( + findings + .iter() + .any(|finding| finding.title.contains("without linked denylist")), + "{findings:?}" + ); + } + + #[test] + fn refresh_logout_without_family_revoke_is_lifecycle_gap() { + let mut report = report_with_artifacts( + vec![artifact( + "artifact_refresh", + ArtifactType::RefreshJwt, + "refresh_token", + LifecycleEvidence { + refresh: vec![EvidenceId("evidence_refresh".to_string())], + ..LifecycleEvidence::default() + }, + )], + vec![ + evidence_with_detector( + "evidence_refresh", + LifecycleStage::Refresh, + "refresh.handler", + 10, + false, + ), + evidence_with_detector( + "evidence_logout", + LifecycleStage::Revoke, + "logout.handler", + 20, + false, + ), + ], + ); + report.lifecycle_paths = link(&report); + + let findings = classify(&report); + let finding = findings + .iter() + .find(|finding| finding.title.contains("without family revocation")) + .expect("refresh family revocation review finding"); + + assert_eq!(finding.category, FindingCategory::LifecycleGap); + assert_eq!(finding.severity, Severity::Medium); + assert!(finding.reviewer_question.is_some()); + } + + #[test] + fn linked_refresh_family_revoke_prevents_logout_gap() { + let mut report = report_with_artifacts( + vec![artifact( + "artifact_refresh", + ArtifactType::RefreshJwt, + "refresh_token", + LifecycleEvidence { + refresh: vec![EvidenceId("evidence_refresh".to_string())], + ..LifecycleEvidence::default() + }, + )], + vec![ + evidence_with_detector( + "evidence_refresh", + LifecycleStage::Refresh, + "refresh.handler", + 10, + false, + ), + evidence_with_detector( + "evidence_logout", + LifecycleStage::Revoke, + "logout.handler", + 20, + false, + ), + evidence_with_excerpt( + "evidence_family_revoke", + LifecycleStage::Revoke, + "refresh.revoke", + 21, + "revokeRefreshFamily(user.id)", + ), + ], + ); + report.lifecycle_paths = link(&report); + + let findings = classify(&report); + + assert!( + !findings + .iter() + .any(|finding| finding.title.contains("without family revocation")), + "{findings:?}" + ); + } + + #[test] + fn sliding_expiry_without_rotation_is_lifecycle_gap() { + let mut report = report_with_artifacts( + vec![artifact( + "artifact_session", + ArtifactType::SessionRecord, + "session", + LifecycleEvidence { + store: vec![EvidenceId("evidence_sliding".to_string())], + ..LifecycleEvidence::default() + }, + )], + vec![evidence_with_excerpt( + "evidence_sliding", + LifecycleStage::Store, + "session.middleware", + 10, + "session({ rolling: true, cookie: { maxAge: 900000 } })", + )], + ); + report.lifecycle_paths = link(&report); + + let findings = classify(&report); + let finding = findings + .iter() + .find(|finding| finding.title.contains("sliding expiry")) + .expect("sliding expiry review finding"); + + assert_eq!(finding.category, FindingCategory::LifecycleGap); + assert_eq!(finding.severity, Severity::Low); + assert!(finding.reviewer_question.is_some()); + } + + #[test] + fn linked_session_rotation_prevents_sliding_expiry_gap() { + let mut report = report_with_artifacts( + vec![artifact( + "artifact_session", + ArtifactType::SessionRecord, + "session", + LifecycleEvidence { + store: vec![EvidenceId("evidence_sliding".to_string())], + refresh: vec![EvidenceId("evidence_rotate".to_string())], + ..LifecycleEvidence::default() + }, + )], + vec![ + evidence_with_excerpt( + "evidence_sliding", + LifecycleStage::Store, + "session.middleware", + 10, + "session({ rolling: true, cookie: { maxAge: 900000 } })", + ), + evidence_with_detector( + "evidence_rotate", + LifecycleStage::Refresh, + "session.regenerate", + 20, + false, + ), + ], + ); + report.lifecycle_paths = link(&report); + + let findings = classify(&report); + + assert!( + !findings + .iter() + .any(|finding| finding.title.contains("sliding expiry")), + "{findings:?}" + ); + } + + #[test] + fn fixed_expiry_session_does_not_trigger_sliding_review() { + let mut report = report_with_artifacts( + vec![artifact( + "artifact_session", + ArtifactType::SessionRecord, + "session", + LifecycleEvidence { + store: vec![EvidenceId("evidence_store".to_string())], + ..LifecycleEvidence::default() + }, + )], + vec![evidence_with_excerpt( + "evidence_store", + LifecycleStage::Store, + "session.middleware", + 10, + "session({ cookie: { maxAge: 900000 } })", + )], + ); + report.lifecycle_paths = link(&report); + + let findings = classify(&report); + + assert!( + !findings + .iter() + .any(|finding| finding.title.contains("sliding expiry")), + "{findings:?}" + ); + } + + #[test] + fn password_change_without_global_revoke_is_lifecycle_gap() { + let mut report = report_with_artifacts( + vec![artifact( + "artifact_password_change", + ArtifactType::Unknown, + "password_change", + LifecycleEvidence { + revoke: vec![EvidenceId("evidence_handler".to_string())], + ..LifecycleEvidence::default() + }, + )], + vec![evidence_with_detector( + "evidence_handler", + LifecycleStage::Revoke, + "password_change.handler", + 10, + false, + )], + ); + report.lifecycle_paths = link(&report); + + let findings = classify(&report); + let finding = findings + .iter() + .find(|finding| finding.title.contains("Password-change handler")) + .expect("password-change global revocation review finding"); + + assert_eq!(finding.category, FindingCategory::LifecycleGap); + assert_eq!(finding.severity, Severity::Medium); + assert!(finding.reviewer_question.is_some()); + } + + #[test] + fn linked_global_revoke_prevents_password_change_gap() { + let mut report = report_with_artifacts( + vec![artifact( + "artifact_password_change", + ArtifactType::Unknown, + "password_change", + LifecycleEvidence { + revoke: vec![EvidenceId("evidence_handler".to_string())], + ..LifecycleEvidence::default() + }, + )], + vec![ + evidence_with_detector( + "evidence_handler", + LifecycleStage::Revoke, + "password_change.handler", + 10, + false, + ), + evidence_with_detector( + "evidence_global_revoke", + LifecycleStage::Revoke, + "password_change.global_revoke", + 12, + false, + ), + ], + ); + report.lifecycle_paths = link(&report); + + let findings = classify(&report); + + assert!( + !findings + .iter() + .any(|finding| finding.title.contains("Password-change handler")), + "{findings:?}" + ); + } + + #[test] + fn current_session_rotation_does_not_prevent_password_change_gap() { + let mut report = report_with_artifacts( + vec![artifact( + "artifact_password_change", + ArtifactType::Unknown, + "password_change", + LifecycleEvidence { + revoke: vec![ + EvidenceId("evidence_handler".to_string()), + EvidenceId("evidence_session_destroy".to_string()), + ], + refresh: vec![EvidenceId("evidence_session_regenerate".to_string())], + ..LifecycleEvidence::default() + }, + )], + vec![ + evidence_with_detector( + "evidence_handler", + LifecycleStage::Revoke, + "password_change.handler", + 10, + false, + ), + evidence_with_detector( + "evidence_session_destroy", + LifecycleStage::Revoke, + "logout.session_destroy", + 12, + false, + ), + evidence_with_detector( + "evidence_session_regenerate", + LifecycleStage::Refresh, + "session.regenerate", + 13, + false, + ), + ], + ); + report.lifecycle_paths = link(&report); + + let findings = classify(&report); + + assert!( + findings + .iter() + .any(|finding| finding.title.contains("Password-change handler")), + "{findings:?}" + ); + } + #[test] fn sort_paths_orders_paths_by_artifact() { let mut paths = vec![ diff --git a/crates/sessionscope-classifier/src/oauth_flow.rs b/crates/sessionscope-classifier/src/oauth_flow.rs index 09fd28e..82dd500 100644 --- a/crates/sessionscope-classifier/src/oauth_flow.rs +++ b/crates/sessionscope-classifier/src/oauth_flow.rs @@ -68,9 +68,8 @@ fn classify_nonce( return Vec::new(); } - let has_nonce = evidence - .iter() - .any(|item| item.detector_id == "oauth.nonce.present"); + let nonce_ids = detector_ids(evidence, "oauth.nonce.present"); + let has_nonce = !nonce_ids.is_empty(); let has_verified = evidence .iter() .any(|item| item.detector_id == "oauth.nonce.verified"); @@ -101,7 +100,7 @@ fn classify_nonce( rule_id: "oidc_nonce_unverified_review", category: FindingCategory::MissingValidationEvidence, severity: Severity::Medium, - evidence_ids: detector_ids(evidence, "oauth.nonce.present"), + evidence_ids: nonce_ids, title: "OIDC nonce is set without visible ID-token nonce verification".to_string(), description: "OIDC flow evidence includes a nonce, but SessionScope did not find same-flow ID-token verification options or comparison evidence for that nonce.".to_string(), suggested_fix: "Pass the expected nonce to the ID-token verification API or compare the validated ID-token nonce claim with the issued nonce.".to_string(), @@ -123,10 +122,7 @@ fn classify_state( let has_state = evidence.iter().any(|item| { matches!( item.detector_id.as_str(), - "oauth.state.present" - | "oauth.state.static" - | "oauth.state.callback_read" - | "oauth.state.verified" + "oauth.state.present" | "oauth.state.static" ) }); let has_verified = evidence @@ -442,6 +438,21 @@ mod tests { assert!(classify(&report).is_empty()); } + #[test] + fn verified_callback_state_does_not_suppress_missing_state() { + let findings = classify(&report_with(&[ + "oauth.flow.auth_code", + "oauth.pkce.present", + "oauth.state.callback_read", + "oauth.state.verified", + ])); + + assert!(findings.iter().any(|finding| { + finding.id.0.contains("oauth_state_missing") + || finding.title.contains("no source-visible state") + })); + } + #[test] fn flags_oidc_nonce_missing_and_unverified() { let missing = classify(&report_with(&[ @@ -494,6 +505,25 @@ mod tests { assert!(verified.is_empty()); } + #[test] + fn verified_oidc_nonce_does_not_suppress_missing_nonce() { + let findings = classify(&report_with(&[ + "oauth.flow.auth_code", + "oauth.pkce.present", + "oauth.state.present", + "oauth.state.verified", + "oauth.oidc.openid_scope", + "oauth.nonce.verified", + ])); + + assert!( + findings + .iter() + .any(|finding| finding.title.contains("no source-visible nonce")), + "{findings:?}" + ); + } + #[test] fn flags_broad_redirect_uri_literals() { let findings = classify(&report_with(&[ diff --git a/crates/sessionscope-cli/tests/cli.rs b/crates/sessionscope-cli/tests/cli.rs index 1c2eb86..0195dda 100644 --- a/crates/sessionscope-cli/tests/cli.rs +++ b/crates/sessionscope-cli/tests/cli.rs @@ -205,6 +205,48 @@ fn explain_known_finding_from_json_report() { assert!(stdout.contains("docs/SCHEMA.md")); } +#[test] +fn explain_sanitizes_deserialized_report_before_rendering() { + let temp = tempfile::tempdir().expect("tempdir should be created"); + let report_path = temp.path().join("scan.json"); + let mut finding = finding_json( + "finding_existing", + "Leaked api_key = PLACEHOLDER_SECRET_DO_NOT_USE", + "Description mentions Authorization: Bearer aaa.bbb.cccccccccccccccccccccc", + "evidence_existing", + 7, + ); + let finding_object = finding.as_object_mut().expect("finding should be object"); + finding_object.insert( + "suggested_fix".to_string(), + serde_json::Value::String( + "Rotate client_secret = PLACEHOLDER_SECRET_DO_NOT_USE".to_string(), + ), + ); + finding_object.insert( + "reviewer_question".to_string(), + serde_json::Value::String("Was token=PLACEHOLDER_SECRET_DO_NOT_USE revoked?".to_string()), + ); + let mut report = scan_report_json(&[finding]); + report["evidence"][0]["excerpt"] = serde_json::Value::String( + "const accessToken = 'PLACEHOLDER_SECRET_DO_NOT_USE';".to_string(), + ); + fs::write(&report_path, report.to_string()).expect("scan report should be written"); + + let output = run_sessionscope(&[ + "explain", + "finding_existing", + "--report", + report_path.to_str().expect("report path should be UTF-8"), + ]); + + assert!(output.status.success()); + let stdout = str::from_utf8(&output.stdout).expect("stdout should be UTF-8"); + assert!(stdout.contains("REDACTED")); + assert!(!stdout.contains("PLACEHOLDER_SECRET_DO_NOT_USE")); + assert!(!stdout.contains("aaa.bbb.cccccccccccccccccccccc")); +} + #[test] fn explain_unknown_finding_does_not_echo_supplied_id() { let temp = tempfile::tempdir().expect("tempdir should be created"); diff --git a/crates/sessionscope-cli/tests/cli_exit_matrix.rs b/crates/sessionscope-cli/tests/cli_exit_matrix.rs new file mode 100644 index 0000000..152414e --- /dev/null +++ b/crates/sessionscope-cli/tests/cli_exit_matrix.rs @@ -0,0 +1,260 @@ +use std::fs; +use std::path::Path; +use std::process::{Command, Output}; + +use serde_json::json; + +#[derive(Clone, Copy)] +struct FindingSpec { + id: &'static str, + severity: &'static str, + category: &'static str, +} + +const HIGH_LIFECYCLE: FindingSpec = FindingSpec { + id: "finding_high_lifecycle", + severity: "high", + category: "lifecycle_gap", +}; +const MEDIUM_MISSING: FindingSpec = FindingSpec { + id: "finding_medium_missing", + severity: "medium", + category: "missing_validation_evidence", +}; +const LOW_DYNAMIC: FindingSpec = FindingSpec { + id: "finding_low_dynamic", + severity: "low", + category: "dynamic_review_required", +}; +const INFO_FRAMEWORK: FindingSpec = FindingSpec { + id: "finding_info_framework", + severity: "info", + category: "framework_default_assumed", +}; + +#[test] +fn documented_exit_code_policy_matrix() { + let cases = [ + MatrixCase { + name: "advisory mode ignores findings", + findings: &[HIGH_LIFECYCLE], + args: &["--mode", "advisory"], + expect_success: true, + }, + MatrixCase { + name: "enforce default blocks high findings", + findings: &[HIGH_LIFECYCLE], + args: &["--mode", "enforce"], + expect_success: false, + }, + MatrixCase { + name: "enforce default allows lower severities", + findings: &[MEDIUM_MISSING, LOW_DYNAMIC, INFO_FRAMEWORK], + args: &["--mode", "enforce"], + expect_success: true, + }, + MatrixCase { + name: "fail severity medium blocks medium", + findings: &[MEDIUM_MISSING], + args: &["--mode", "enforce", "--fail-severity", "medium"], + expect_success: false, + }, + MatrixCase { + name: "fail severity low blocks low", + findings: &[LOW_DYNAMIC], + args: &["--mode", "enforce", "--fail-severity", "low"], + expect_success: false, + }, + MatrixCase { + name: "fail severity info blocks info", + findings: &[INFO_FRAMEWORK], + args: &["--mode", "enforce", "--fail-severity", "info"], + expect_success: false, + }, + MatrixCase { + name: "category filter excludes nonmatching category", + findings: &[HIGH_LIFECYCLE], + args: &[ + "--mode", + "enforce", + "--fail-category", + "missing_validation_evidence", + ], + expect_success: true, + }, + MatrixCase { + name: "category filter accepts comma-separated categories", + findings: &[INFO_FRAMEWORK], + args: &[ + "--mode", + "enforce", + "--fail-severity", + "info", + "--fail-category", + "dynamic_review_required,framework_default_assumed", + ], + expect_success: false, + }, + MatrixCase { + name: "empty category filter behaves like no category filter", + findings: &[HIGH_LIFECYCLE], + args: &["--mode", "enforce", "--fail-category", ""], + expect_success: false, + }, + MatrixCase { + name: "include finding id blocks below threshold", + findings: &[LOW_DYNAMIC], + args: &[ + "--mode", + "enforce", + "--include-finding-id", + "finding_low_dynamic", + ], + expect_success: false, + }, + MatrixCase { + name: "exclude finding id wins over include", + findings: &[HIGH_LIFECYCLE], + args: &[ + "--mode", + "enforce", + "--include-finding-id", + "finding_high_lifecycle", + "--exclude-finding-id", + "finding_high_lifecycle", + ], + expect_success: true, + }, + ]; + + for case in cases { + run_matrix_case(case); + } +} + +#[test] +fn baseline_suppression_and_include_precedence_are_documented() { + let temp = tempfile::tempdir().expect("tempdir should be created"); + let report_path = temp.path().join("report.json"); + let baseline_path = temp.path().join("baseline.json"); + write_report(&report_path, &[HIGH_LIFECYCLE]); + write_report(&baseline_path, &[HIGH_LIFECYCLE]); + + let suppressed = run_sessionscope_in( + temp.path(), + &[ + "evaluate", + report_path.to_str().expect("path should be UTF-8"), + "--no-policy-config", + "--mode", + "enforce", + "--baseline", + baseline_path.to_str().expect("path should be UTF-8"), + ], + ); + assert!( + suppressed.status.success(), + "baseline should suppress matching high finding: {}", + String::from_utf8_lossy(&suppressed.stderr) + ); + + let included = run_sessionscope_in( + temp.path(), + &[ + "evaluate", + report_path.to_str().expect("path should be UTF-8"), + "--no-policy-config", + "--mode", + "enforce", + "--baseline", + baseline_path.to_str().expect("path should be UTF-8"), + "--include-finding-id", + "finding_high_lifecycle", + ], + ); + assert!( + !included.status.success(), + "include-finding-id should block unless excluded, even when baseline matches" + ); +} + +struct MatrixCase { + name: &'static str, + findings: &'static [FindingSpec], + args: &'static [&'static str], + expect_success: bool, +} + +fn run_matrix_case(case: MatrixCase) { + let temp = tempfile::tempdir().expect("tempdir should be created"); + let report_path = temp.path().join("report.json"); + write_report(&report_path, case.findings); + + let mut args = vec![ + "evaluate".to_string(), + report_path + .to_str() + .expect("path should be UTF-8") + .to_string(), + "--no-policy-config".to_string(), + ]; + args.extend(case.args.iter().map(|arg| arg.to_string())); + let borrowed = args.iter().map(String::as_str).collect::>(); + let output = run_sessionscope_in(temp.path(), &borrowed); + + assert_eq!( + output.status.success(), + case.expect_success, + "{}: stdout={} stderr={}", + case.name, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +fn write_report(path: &Path, findings: &[FindingSpec]) { + let findings = findings + .iter() + .map(|finding| { + json!({ + "id": finding.id, + "category": finding.category, + "severity": finding.severity, + "artifact_ids": [], + "evidence_ids": [], + "title": format!("{} {}", finding.severity, finding.category), + "description": "policy matrix fixture", + "suggested_fix": null, + "reviewer_question": null + }) + }) + .collect::>(); + let report = json!({ + "schema_version": "0.5.0", + "summary": { + "files_discovered": 0, + "files_scanned": 0, + "files_skipped": 0, + "diagnostics": [], + "worker_panic_count": 0 + }, + "files": [], + "artifacts": [], + "evidence": [], + "lifecycle_paths": [], + "findings": findings + }); + fs::write( + path, + serde_json::to_string_pretty(&report).expect("report serializes"), + ) + .expect("report should be written"); +} + +fn run_sessionscope_in(cwd: &Path, args: &[&str]) -> Output { + Command::new(env!("CARGO_BIN_EXE_sessionscope")) + .current_dir(cwd) + .args(args) + .output() + .expect("failed to run sessionscope") +} diff --git a/crates/sessionscope-core/src/redaction.rs b/crates/sessionscope-core/src/redaction.rs index 35624a6..4f0cfcd 100644 --- a/crates/sessionscope-core/src/redaction.rs +++ b/crates/sessionscope-core/src/redaction.rs @@ -56,7 +56,7 @@ static QUOTED_LITERAL_RE: LazyLock = LazyLock::new(|| { }); static SENSITIVE_QUOTED_ASSIGNMENT_RE: LazyLock = LazyLock::new(|| { Regex::new( - r#"(?ix)(["']?\b(?:access[_-]?token|refresh[_-]?token|id[_-]?token|reset[_-]?token|session[_-]?token|csrf[_-]?token|api[_-]?key|apikey|secret|client[_-]?secret|password|passwd|jwt|sessionid|private[_-]?key|signing[_-]?key|state|nonce|code[_-]?verifier|code[_-]?challenge|codeVerifier|codeChallenge)\b["']?\s*[:=]\s*)(["'])([^"']*)(["'])"#, + r#"(?ix)(["']?\b(?:access[_-]?token|refresh[_-]?token|id[_-]?token|reset[_-]?token|session[_-]?token|csrf[_-]?token|api[_-]?key|apikey|secret|client[_-]?secret|clientSecret|password|passwd|jwt|sessionid|private[_-]?key|signing[_-]?key|state|nonce|code[_-]?verifier|code[_-]?challenge|codeVerifier|codeChallenge)\b["']?\s*[:=]\s*)(["'])([^"']*)(["'])"#, ) .expect("sensitive quoted assignment regex should compile") }); @@ -743,6 +743,14 @@ mod tests { assert!(!output.contains("abcdefghijklmno")); } + #[test] + fn redacts_quoted_camel_case_client_secret() { + let output = redact_sensitive_values("const config = { clientSecret: \"short\" }"); + + assert!(output.contains("[REDACTED]")); + assert!(!output.contains("short")); + } + #[test] fn redacts_placeholder_secret_values() { let output = redact_sensitive_values( diff --git a/crates/sessionscope-core/src/source.rs b/crates/sessionscope-core/src/source.rs index c0467ad..1d07eb5 100644 --- a/crates/sessionscope-core/src/source.rs +++ b/crates/sessionscope-core/src/source.rs @@ -93,9 +93,18 @@ fn read_source_checked( } String::from_utf8(bytes) + .map(normalize_line_endings) .map_err(|_| SkippedReason::ReadError(format!("{}", io::ErrorKind::InvalidData))) } +fn normalize_line_endings(source: String) -> String { + if source.contains('\r') { + source.replace("\r\n", "\n").replace('\r', "\n") + } else { + source + } +} + fn validate_source_path(path: &Path, canonical_root: Option<&Path>) -> Result<(), SkippedReason> { let metadata = fs::symlink_metadata(path).map_err(io_kind_skipped_reason)?; if metadata.file_type().is_symlink() { @@ -199,6 +208,17 @@ mod tests { assert_eq!(result, contents); } + #[test] + fn normalizes_windows_line_endings_for_deterministic_scans() { + let temp = tempdir().expect("tempdir should be created"); + let path = temp.path().join("windows.ts"); + fs::write(&path, "const a = 1;\r\nconst b = 2;\r\n").expect("file should be written"); + + let result = read_source(&path, 1_000).expect("file inside cap should read"); + + assert_eq!(result, "const a = 1;\nconst b = 2;\n"); + } + /// Simulates a file that grows on disk between the metadata size check and /// the body read. We do this by passing a `max_file_size_bytes` cap smaller /// than the file's actual size after we extend it — the `take()`-guarded diff --git a/crates/sessionscope-detectors/src/client_storage/mod.rs b/crates/sessionscope-detectors/src/client_storage/mod.rs index 4913936..a9298ea 100644 --- a/crates/sessionscope-detectors/src/client_storage/mod.rs +++ b/crates/sessionscope-detectors/src/client_storage/mod.rs @@ -24,8 +24,9 @@ static STORAGE_VALUE_RE: LazyLock = LazyLock::new(|| { static DOCUMENT_COOKIE_RE: LazyLock = LazyLock::new(|| { Regex::new(r#"(?i)document\s*\.\s*cookie\s*="#).expect("document cookie regex should compile") }); -static COOKIE_KEY_RE: LazyLock = - LazyLock::new(|| Regex::new(r#"(["'])([^="';]+)="#).expect("cookie key regex should compile")); +static COOKIE_KEY_RE: LazyLock = LazyLock::new(|| { + Regex::new(r#"(["'`])([^="';`]+)="#).expect("cookie key regex should compile") +}); static URL_PATH_FRAGMENT_RE: LazyLock = LazyLock::new(|| { Regex::new(r#"(?i)(#(?:access[_-]?token|id[_-]?token|refresh[_-]?token|jwt|bearer|session)\s*=|/(?:access[_-]?token|id[_-]?token|refresh[_-]?token|jwt|bearer)(?:=|/)(?:\$\{|[A-Za-z0-9._~+%-]))"#) .expect("url path/fragment regex should compile") @@ -345,6 +346,7 @@ mod tests { localStorage.setItem('access_token', token) sessionStorage.setItem('refresh_token', refresh) document.cookie = "session=" + sessionId +document.cookie = `access_token=${accessToken}` const url = `/callback#id_token=${idToken}` const clientSecret = 'PLACEHOLDER_SECRET_DO_NOT_USE' "#, diff --git a/crates/sessionscope-detectors/src/oauth_flow/mod.rs b/crates/sessionscope-detectors/src/oauth_flow/mod.rs index ea36f2c..a4e12f4 100644 --- a/crates/sessionscope-detectors/src/oauth_flow/mod.rs +++ b/crates/sessionscope-detectors/src/oauth_flow/mod.rs @@ -48,7 +48,7 @@ static REDIRECT_RE: LazyLock = LazyLock::new(|| { static QUOTED_RE: LazyLock = LazyLock::new(|| Regex::new(r#"["']([^"']+)["']"#).expect("quoted regex should compile")); static OAUTH_VALUE_RE: LazyLock = LazyLock::new(|| { - Regex::new(r#"(?ix)(\b(?:state|nonce|code_verifier|codeVerifier|code_challenge|codeChallenge)\b\s*[:=]\s*)(["'`])([^"'`]{8,})(["'`])"#) + Regex::new(r#"(?ix)(\b(?:state|nonce|code_verifier|codeVerifier|code_challenge|codeChallenge|client_secret|clientSecret|secret)\b\s*[:=]\s*)(["'`])([^"'`]+)(["'`])"#) .expect("oauth value regex should compile") }); static OAUTH_URL_VALUE_RE: LazyLock = LazyLock::new(|| { @@ -139,14 +139,25 @@ fn detect(input: &DetectorInput<'_>) -> DetectionOutput { } else { "oauth.state.present" }; - signals.push(signal( + if matches!( detector_id, - LifecycleStage::Validate, - line, - line_number, - !STATIC_STATE_RE.is_match(line), - false, - )); + "oauth.state.callback_read" | "oauth.state.verified" + ) || is_state_issue_line(line) + { + let stage = if matches!(detector_id, "oauth.state.present" | "oauth.state.static") { + LifecycleStage::Issue + } else { + LifecycleStage::Validate + }; + signals.push(signal( + detector_id, + stage, + line, + line_number, + !STATIC_STATE_RE.is_match(line), + false, + )); + } } if STATE_VERIFY_RE.is_match(line) { signals.push(signal( @@ -168,7 +179,7 @@ fn detect(input: &DetectorInput<'_>) -> DetectionOutput { false, )); } - if NONCE_RE.is_match(line) { + if is_nonce_issue_line(line) { signals.push(signal( "oauth.nonce.present", LifecycleStage::Issue, @@ -387,7 +398,7 @@ fn is_import_only_line(line: &str) -> bool { fn redirect_line_is_broad(line: &str) -> bool { QUOTED_RE.captures_iter(line).any(|capture| { let value = capture.get(1).map_or("", |capture| capture.as_str()); - if is_loopback_redirect(value) { + if is_loopback_redirect(value) && !value.contains('*') { return false; } value.contains('*') || bare_host(value) || top_level_wildcard(value) @@ -398,6 +409,32 @@ fn is_loopback_redirect(value: &str) -> bool { value.contains("localhost") || value.contains("127.0.0.1") || value.contains("[::1]") } +fn is_nonce_issue_line(line: &str) -> bool { + if !NONCE_RE.is_match(line) { + return false; + } + let normalized = line.to_ascii_lowercase(); + normalized.contains("authorizationurl") + || normalized.contains("authorization_url") + || normalized.contains("authorize_redirect") + || normalized.contains("oauthprovider") + || normalized.contains("oidcprovider") + || (normalized.contains("response_type") && normalized.contains("code")) +} + +fn is_state_issue_line(line: &str) -> bool { + if !STATE_RE.is_match(line) { + return false; + } + let normalized = line.to_ascii_lowercase(); + normalized.contains("authorizationurl") + || normalized.contains("authorization_url") + || normalized.contains("authorize_redirect") + || normalized.contains("oauthprovider") + || normalized.contains("oidcprovider") + || (normalized.contains("response_type") && normalized.contains("code")) +} + fn bare_host(value: &str) -> bool { let Some(rest) = value .strip_prefix("https://") @@ -443,7 +480,7 @@ mod tests { fn detects_auth_code_flow_with_pkce_state_and_redirect_evidence() { let output = OAuthFlowDetector.detect(&input( r#" -const url = client.authorizationUrl({ response_type: 'code', scope: 'openid profile', state: crypto.randomUUID(), code_challenge: challenge, redirect_uris: ['https://*.example.com'] }); +const url = client.authorizationUrl({ response_type: 'code', scope: 'openid profile', state: crypto.randomUUID(), nonce: crypto.randomUUID(), code_challenge: challenge, redirect_uris: ['https://*.example.com'] }); if (req.query.state === session.oauthState) {} verifyIdToken(token, { nonce }) "#, @@ -519,11 +556,110 @@ const url = client.authorizationUrl({ response_type: 'code', state: makeState("c assert!(!redirect_line_is_broad( "redirect_uris: ['http://localhost:3000']" )); + assert!(redirect_line_is_broad( + "redirect_uris: ['http://localhost:3000/*']" + )); assert!(redirect_line_is_broad( "redirect_uris: ['https://example.com']" )); } + #[test] + fn does_not_treat_nonce_verification_as_nonce_issue_evidence() { + let output = OAuthFlowDetector.detect(&input( + r#" +const url = client.authorizationUrl({ response_type: 'code', scope: 'openid', state, code_challenge: challenge }) +verifyIdToken(token, { nonce }) +"#, + )); + + assert!( + !output + .evidence + .iter() + .any(|evidence| evidence.detector_id == "oauth.nonce.present") + ); + assert!( + output + .evidence + .iter() + .any(|evidence| evidence.detector_id == "oauth.nonce.verified") + ); + } + + #[test] + fn does_not_treat_session_state_reads_as_state_issue_evidence() { + let output = OAuthFlowDetector.detect(&input( + r#" +const url = client.authorizationUrl({ response_type: 'code', code_challenge: challenge }) +const expectedState = session.oauthState +if (req.query.state === expectedState) {} +"#, + )); + + assert!( + !output + .evidence + .iter() + .any(|evidence| evidence.detector_id == "oauth.state.present") + ); + assert!( + output + .evidence + .iter() + .any(|evidence| evidence.detector_id == "oauth.state.callback_read") + ); + assert!( + output + .evidence + .iter() + .any(|evidence| evidence.detector_id == "oauth.state.verified") + ); + } + + #[test] + fn detects_nonce_issue_evidence_when_nonce_precedes_response_type() { + let output = OAuthFlowDetector.detect(&input( + r#" +const url = client.authorizationUrl({ nonce, state, response_type: 'code', scope: 'openid', code_challenge: challenge }) +"#, + )); + + assert!( + output + .evidence + .iter() + .any(|evidence| evidence.detector_id == "oauth.nonce.present") + ); + } + + #[test] + fn detects_state_issue_evidence_when_state_precedes_response_type() { + let output = OAuthFlowDetector.detect(&input( + r#" +const url = client.authorizationUrl({ state, response_type: 'code', code_challenge: challenge }) +"#, + )); + + let state_evidence = output + .evidence + .iter() + .find(|evidence| evidence.detector_id == "oauth.state.present") + .expect("state issue evidence"); + assert_eq!(state_evidence.lifecycle_stage, LifecycleStage::Issue); + } + + #[test] + fn redacts_oauth_client_secret_values_in_excerpts() { + let output = OAuthFlowDetector.detect(&input( + r#"new OAuth2Strategy({ response_type: 'code', clientSecret: "short" })"#, + )); + let rendered = format!("{:?}", output.evidence); + + assert!(rendered.contains("[REDACTED]")); + assert!(!rendered.contains("short")); + } + #[test] fn does_not_mark_state_or_nonce_reads_as_verification() { let output = OAuthFlowDetector.detect(&input( diff --git a/crates/sessionscope-detectors/src/sessions/mod.rs b/crates/sessionscope-detectors/src/sessions/mod.rs index ed776d2..8c3ac10 100644 --- a/crates/sessionscope-detectors/src/sessions/mod.rs +++ b/crates/sessionscope-detectors/src/sessions/mod.rs @@ -562,7 +562,20 @@ fn collect_js_signals(node: Node<'_>, source: &str, signals: &mut Vec) { "function_declaration" | "method_definition" => { let name = js_function_name(node, source).unwrap_or_default(); let normalized = normalize_symbol(&name); - if is_auth_handler_context(&normalized) { + if is_password_change_handler_context(&normalized) { + signals.push(signal( + SignalSpec::revoke( + "password_change.handler", + ArtifactType::Unknown, + "password_change", + "javascript", + Confidence::Medium, + true, + ), + node, + source, + )); + } else if is_auth_handler_context(&normalized) { signals.push(session_fixation_signal( "session.auth_transition", LifecycleStage::Issue, @@ -603,7 +616,20 @@ fn collect_js_signals(node: Node<'_>, source: &str, signals: &mut Vec) { )); } let normalized = normalize_symbol_without_literals(&text); - if is_auth_handler_context(&normalized) { + if is_password_change_handler_context(&normalized) { + signals.push(signal( + SignalSpec::revoke( + "password_change.handler", + ArtifactType::Unknown, + "password_change", + "nextjs", + Confidence::Medium, + true, + ), + node, + source, + )); + } else if is_auth_handler_context(&normalized) { signals.push(session_fixation_signal( "session.auth_transition", LifecycleStage::Issue, @@ -796,6 +822,21 @@ fn collect_js_call_signal(node: Node<'_>, source: &str, signals: &mut Vec, source: &str, signals: &mut Vec, source: &str, signals: &mut Vec bool { || normalized.contains("destroysession") } +fn is_password_change_handler_context(normalized: &str) -> bool { + (normalized.contains("passwordchange") + || normalized.contains("password_change") + || normalized.contains("change_password") + || normalized.contains("changepassword") + || normalized.contains("updatepassword") + || normalized.contains("update_password") + || normalized.contains("password_update") + || normalized.contains("passwordupdate")) + && !normalized.contains("validate") + && !normalized.contains("validator") + && !normalized.contains("verify") + && !normalized.contains("check") + && !normalized.contains("strength") +} + +fn is_global_password_change_revocation_call(normalized: &str) -> bool { + (normalized.contains("revokeallsessions") + || normalized.contains("revoke_all_sessions") + || normalized.contains("invalidateallsessions") + || normalized.contains("invalidate_all_sessions") + || normalized.contains("bump token version") + || normalized.contains("bumptokenversion") + || normalized.contains("bump_token_version") + || normalized.contains("tokenversion") + || normalized.contains("token_version")) + || (normalized.contains("refresh") + && (normalized.contains("user") + || normalized.contains("family") + || normalized.contains("allsessions") + || normalized.contains("all_sessions")) + && (normalized.contains("revoke") + || normalized.contains("delete") + || normalized.contains("invalidate") + || normalized.contains("destroy"))) +} + fn is_js_provider_revoke_call(normalized: &str) -> bool { is_provider_revoke_text(normalized) } diff --git a/crates/sessionscope-testing/src/fixtures.rs b/crates/sessionscope-testing/src/fixtures.rs index 846c377..521ca4e 100644 --- a/crates/sessionscope-testing/src/fixtures.rs +++ b/crates/sessionscope-testing/src/fixtures.rs @@ -132,13 +132,15 @@ mod tests { assert!(!case.expected.framework.is_empty()); assert!(!case.expected.notes.is_empty()); assert!(!case.expected.source_files.is_empty()); - assert!( - !case.expected.expected_artifacts.is_empty() - || !case.expected.expected_lifecycle_stages.is_empty() - || !case.expected.expected_findings.is_empty(), - "{} should include at least one expectation", - case.expected.fixture_id - ); + if !case.expected.fixture_id.contains("clean-baseline") { + assert!( + !case.expected.expected_artifacts.is_empty() + || !case.expected.expected_lifecycle_stages.is_empty() + || !case.expected.expected_findings.is_empty(), + "{} should include at least one expectation", + case.expected.fixture_id + ); + } for source_file in &case.expected.source_files { assert!( @@ -762,6 +764,87 @@ mod tests { } } + #[test] + fn clean_baseline_fixtures_have_no_findings() { + const NEW_P1_P4_CHECK_IDS: &[&str] = &[ + "cookie_host_prefix_path_violation", + "cookie_host_prefix_domain_violation", + "cookie_host_prefix_secure_violation", + "cookie_secure_prefix_secure_violation", + "cookie_samesite_none_without_secure", + "cookie_partitioned_review", + "cookie_domain_leak_review", + "cookie_conflicting_writes_review", + "jwt_alg_none_accepted", + "jwt_alg_confusion_signal", + "jwt_jku_header_trust", + "jwt_x5u_header_trust", + "jwt_embedded_jwk_trust", + "jwt_nbf_missing", + "jwt_clock_skew_review", + "jwt_kid_unvalidated_review", + "oauth_pkce_missing_review", + "oauth_state_missing", + "oauth_state_static_review", + "oauth_state_unverified_review", + "oidc_nonce_missing", + "oidc_nonce_unverified_review", + "oauth_redirect_uri_wildcard_review", + "token_in_local_storage", + "token_in_session_storage", + "token_in_url_path_or_fragment", + "client_secret_in_browser_code", + "jwt_denylist_absent_on_logout_review", + "refresh_family_revocation_absent_on_logout_review", + "sliding_expiry_without_rotation_review", + "password_change_global_revocation_absent_review", + ]; + let roots = [ + fixture_root() + .join("express") + .join("clean-baseline-lifecycle"), + fixture_root() + .join("nextjs") + .join("clean-baseline-oauth-storage"), + fixture_root() + .join("fastapi") + .join("clean-baseline-lifecycle"), + fixture_root() + .join("django") + .join("clean-baseline-password-refresh"), + fixture_root().join("generic-js").join("clean-baseline-jwt"), + fixture_root() + .join("generic-ts") + .join("clean-baseline-jwt-oauth"), + fixture_root() + .join("generic-python") + .join("clean-baseline-jwt-refresh"), + ]; + + assert!( + NEW_P1_P4_CHECK_IDS.len() >= 31, + "new check inventory should stay explicit" + ); + + for root in roots { + let report = classify( + scan_path( + ScanConfig::new(&root), + Arc::new(DetectorRegistry::builtin()), + ) + .unwrap_or_else(|error| panic!("{} should scan: {error}", root.display())), + ); + + assert!( + report.findings.is_empty(), + "{} should not produce any findings, including {:?}; got {:?}", + root.display(), + NEW_P1_P4_CHECK_IDS, + report.findings + ); + } + } + #[test] fn logout_fixtures_emit_revoke_evidence() { let cases = [ @@ -830,6 +913,53 @@ mod tests { })); } + #[test] + fn jwt_logout_without_denylist_fixture_produces_review_gap() { + let root = fixture_root() + .join("generic-ts") + .join("jwt-logout-without-denylist"); + let report = classify( + scan_path( + ScanConfig::new(&root), + Arc::new(DetectorRegistry::builtin()), + ) + .expect("jwt logout without denylist fixture should scan"), + ); + + assert!(report.findings.iter().any(|finding| { + finding.category == FindingCategory::LifecycleGap + && finding.title.contains("without linked denylist") + && finding.reviewer_question.is_some() + })); + } + + #[test] + fn jwt_logout_with_denylist_fixture_stays_clean_for_denylist_gap() { + let root = fixture_root() + .join("generic-ts") + .join("jwt-logout-with-denylist"); + let report = classify( + scan_path( + ScanConfig::new(&root), + Arc::new(DetectorRegistry::builtin()), + ) + .expect("jwt logout with denylist fixture should scan"), + ); + + assert!(report.evidence.iter().any(|evidence| { + evidence.lifecycle_stage == LifecycleStage::Revoke + && evidence.detector_id == "logout.token_revoke" + })); + assert!( + !report + .findings + .iter() + .any(|finding| finding.title.contains("without linked denylist")), + "{:?}", + report.findings + ); + } + #[test] fn issue_27_provider_library_fixtures_emit_documented_coverage() { let cases = [ @@ -1044,6 +1174,239 @@ mod tests { })); } + #[test] + fn logout_refresh_family_missing_fixture_produces_review_gap() { + let root = fixture_root() + .join("express") + .join("logout-refresh-family-missing"); + let report = classify( + scan_path( + ScanConfig::new(&root), + Arc::new(DetectorRegistry::builtin()), + ) + .expect("logout refresh family missing fixture should scan"), + ); + + assert!(report.findings.iter().any(|finding| { + finding.category == FindingCategory::LifecycleGap + && finding.title.contains("without family revocation") + && finding.reviewer_question.is_some() + })); + } + + #[test] + fn logout_refresh_family_revoked_fixture_stays_clean_for_family_gap() { + let root = fixture_root() + .join("express") + .join("logout-refresh-family-revoked"); + let report = classify( + scan_path( + ScanConfig::new(&root), + Arc::new(DetectorRegistry::builtin()), + ) + .expect("logout refresh family revoked fixture should scan"), + ); + + assert!(report.evidence.iter().any(|evidence| { + evidence.lifecycle_stage == LifecycleStage::Revoke + && evidence.detector_id == "refresh.revoke" + })); + assert!( + !report + .findings + .iter() + .any(|finding| finding.title.contains("without family revocation")), + "{:?}", + report.findings + ); + } + + #[test] + fn sliding_expiry_fixture_produces_rotation_review_gap() { + let root = fixture_root() + .join("express") + .join("sliding-expiry-without-rotation"); + let report = classify( + scan_path( + ScanConfig::new(&root), + Arc::new(DetectorRegistry::builtin()), + ) + .expect("sliding expiry fixture should scan"), + ); + + assert!(report.findings.iter().any(|finding| { + finding.category == FindingCategory::LifecycleGap + && finding.title.contains("sliding expiry") + && finding.reviewer_question.is_some() + })); + } + + #[test] + fn sliding_expiry_with_rotation_fixture_stays_clean_for_rotation_gap() { + let root = fixture_root() + .join("express") + .join("sliding-expiry-with-rotation"); + let report = classify( + scan_path( + ScanConfig::new(&root), + Arc::new(DetectorRegistry::builtin()), + ) + .expect("sliding expiry with rotation fixture should scan"), + ); + + assert!(report.evidence.iter().any(|evidence| { + evidence.lifecycle_stage == LifecycleStage::Refresh + && evidence.detector_id == "session.regenerate" + })); + assert!( + !report + .findings + .iter() + .any(|finding| finding.title.contains("sliding expiry")), + "{:?}", + report.findings + ); + } + + #[test] + fn fixed_expiry_fixture_stays_clean_for_sliding_review() { + let root = fixture_root().join("express").join("fixed-expiry-session"); + let report = classify( + scan_path( + ScanConfig::new(&root), + Arc::new(DetectorRegistry::builtin()), + ) + .expect("fixed expiry fixture should scan"), + ); + + assert!( + !report + .findings + .iter() + .any(|finding| finding.title.contains("sliding expiry")), + "{:?}", + report.findings + ); + } + + #[test] + fn password_change_without_global_revoke_fixture_produces_review_gap() { + let root = fixture_root() + .join("django") + .join("password-change-without-global-revoke"); + let report = classify( + scan_path( + ScanConfig::new(&root), + Arc::new(DetectorRegistry::builtin()), + ) + .expect("password-change without global revoke fixture should scan"), + ); + + assert!(report.evidence.iter().any(|evidence| { + evidence.lifecycle_stage == LifecycleStage::Revoke + && evidence.detector_id == "password_change.handler" + })); + assert!(report.findings.iter().any(|finding| { + finding.category == FindingCategory::LifecycleGap + && finding.title.contains("Password-change handler") + && finding.reviewer_question.is_some() + })); + } + + #[test] + fn password_change_global_revoke_fixture_stays_clean_for_global_gap() { + let root = fixture_root() + .join("django") + .join("password-change-global-revoke"); + let report = classify( + scan_path( + ScanConfig::new(&root), + Arc::new(DetectorRegistry::builtin()), + ) + .expect("password-change global revoke fixture should scan"), + ); + + assert!(report.evidence.iter().any(|evidence| { + evidence.lifecycle_stage == LifecycleStage::Revoke + && evidence.detector_id == "password_change.global_revoke" + })); + assert!( + !report + .findings + .iter() + .any(|finding| finding.title.contains("Password-change handler")), + "{:?}", + report.findings + ); + } + + #[test] + fn password_change_current_session_only_fixture_still_produces_review_gap() { + let root = fixture_root() + .join("django") + .join("password-change-current-session-only"); + let report = classify( + scan_path( + ScanConfig::new(&root), + Arc::new(DetectorRegistry::builtin()), + ) + .expect("password-change current-session-only fixture should scan"), + ); + + assert!(report.evidence.iter().any(|evidence| { + evidence.lifecycle_stage == LifecycleStage::Revoke + && evidence.detector_id == "password_change.handler" + })); + assert!(report.evidence.iter().any(|evidence| { + evidence.lifecycle_stage == LifecycleStage::Refresh + && evidence.detector_id == "session.regenerate" + })); + assert!( + !report + .evidence + .iter() + .any(|evidence| evidence.detector_id == "password_change.global_revoke"), + "{:?}", + report.evidence + ); + assert!(report.findings.iter().any(|finding| { + finding.category == FindingCategory::LifecycleGap + && finding.title.contains("Password-change handler") + && finding.reviewer_question.is_some() + })); + } + + #[test] + fn password_validation_utility_fixture_does_not_emit_password_change_review() { + let root = fixture_root() + .join("generic-python") + .join("password-validation-utility"); + let report = classify( + scan_path( + ScanConfig::new(&root), + Arc::new(DetectorRegistry::builtin()), + ) + .expect("password validation utility fixture should scan"), + ); + + assert!( + !report + .evidence + .iter() + .any(|evidence| evidence.detector_id == "password_change.handler"), + "{:?}", + report.evidence + ); + assert!( + !report + .findings + .iter() + .any(|finding| finding.title.contains("Password-change handler")), + "{:?}", + report.findings + ); + } + #[test] fn unrelated_refresh_fixtures_do_not_satisfy_each_other() { let root = fixture_root().join("express"); diff --git a/crates/sessionscope-testing/src/snapshots.rs b/crates/sessionscope-testing/src/snapshots.rs index d9b3b4e..94af384 100644 --- a/crates/sessionscope-testing/src/snapshots.rs +++ b/crates/sessionscope-testing/src/snapshots.rs @@ -1,3 +1,21 @@ pub fn normalize_snapshot_paths(input: &str) -> String { - input.replace('\\', "/") + input + .replace("\r\n", "\n") + .replace("\\r\\n", "\\n") + .replace('\\', "/") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn normalizes_windows_line_endings_and_paths() { + assert_eq!( + normalize_snapshot_paths( + "{\r\n \"path\": \"dir\\file.ts\", \"excerpt\": \"a\\r\\nb\"\r\n}" + ), + "{\n \"path\": \"dir/file.ts\", \"excerpt\": \"a/nb\"\n}" + ); + } } diff --git a/crates/sessionscope-testing/tests/json_snapshots.rs b/crates/sessionscope-testing/tests/json_snapshots.rs new file mode 100644 index 0000000..79a1050 --- /dev/null +++ b/crates/sessionscope-testing/tests/json_snapshots.rs @@ -0,0 +1,101 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use sessionscope_classifier::classify; +use sessionscope_core::{ScanConfig, scan_path}; +use sessionscope_detectors::DetectorRegistry; +use sessionscope_reporters::{ReportFormat, render}; +use sessionscope_testing::fixtures::fixture_root; +use sessionscope_testing::snapshots::normalize_snapshot_paths; + +const SNAPSHOT_CASES: &[SnapshotCase] = &[ + SnapshotCase { + name: "express-cookie-session-lifecycle", + fixture_segments: &["express", "cookie-session-lifecycle"], + }, + SnapshotCase { + name: "nextjs-route-handler-auth", + fixture_segments: &["nextjs", "route-handler-auth"], + }, + SnapshotCase { + name: "fastapi-dependency-auth-lifecycle", + fixture_segments: &["fastapi", "dependency-auth-lifecycle"], + }, + SnapshotCase { + name: "django-session-and-reset-flow", + fixture_segments: &["django", "session-and-reset-flow"], + }, + SnapshotCase { + name: "generic-js-jwt-crypto-trust-alg-none", + fixture_segments: &["generic-js", "jwt-crypto-trust-alg-none"], + }, + SnapshotCase { + name: "generic-ts-jwt-validation", + fixture_segments: &["generic-ts", "jwt-validation"], + }, + SnapshotCase { + name: "generic-python-jwt-and-reset", + fixture_segments: &["generic-python", "jwt-and-reset"], + }, +]; + +struct SnapshotCase { + name: &'static str, + fixture_segments: &'static [&'static str], +} + +#[test] +fn json_snapshots_match_representative_framework_fixtures() { + let update = std::env::var_os("SESSIONSCOPE_UPDATE_JSON_SNAPSHOTS").is_some(); + let snapshot_root = snapshot_root(); + + for case in SNAPSHOT_CASES { + let rendered = render_snapshot(case); + let snapshot_path = snapshot_root.join(format!("{}.json", case.name)); + + if update { + fs::write(&snapshot_path, &rendered).unwrap_or_else(|error| { + panic!("failed to update {}: {error}", snapshot_path.display()) + }); + continue; + } + + let expected = fs::read_to_string(&snapshot_path).unwrap_or_else(|error| { + panic!( + "failed to read snapshot {}: {error}", + snapshot_path.display() + ) + }); + assert_eq!( + normalize_snapshot_paths(&expected), + normalize_snapshot_paths(&rendered), + "JSON snapshot mismatch for {}; regenerate with `SESSIONSCOPE_UPDATE_JSON_SNAPSHOTS=1 cargo test -p sessionscope-testing --test json_snapshots`", + case.name + ); + } +} + +fn render_snapshot(case: &SnapshotCase) -> String { + let root = case + .fixture_segments + .iter() + .fold(fixture_root(), |path, segment| path.join(segment)); + let report = classify( + scan_path( + ScanConfig::new(&root), + Arc::new(DetectorRegistry::builtin()), + ) + .unwrap_or_else(|error| panic!("{} should scan: {error}", root.display())), + ); + render(&report, ReportFormat::Json) +} + +fn snapshot_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("tests") + .join("integration") + .join("snapshots") +} diff --git a/docs/COVERAGE_MATRIX.md b/docs/COVERAGE_MATRIX.md index 6c4b20c..a6c16e7 100644 --- a/docs/COVERAGE_MATRIX.md +++ b/docs/COVERAGE_MATRIX.md @@ -1,6 +1,6 @@ # Coverage Matrix -This matrix is the per-check source of truth for what SessionScope can find on a scanned project. SessionScope is offline-only and evidence-bound: **supported** means deterministic source patterns are recognized, **review-required** means dynamic or framework-default behavior is surfaced for reviewer confirmation, and **not covered** means the stack or pattern is intentionally out of scope for this release round. +This matrix is the per-check source of truth for what SessionScope can find on a scanned project. To decide whether a check fires on your stack, find the check ID, then read across the Languages, Frameworks, Libraries/SDKs, and Triggering APIs columns. SessionScope is offline-only and evidence-bound: **supported** means deterministic source patterns are recognized, **review-required** means dynamic or framework-default behavior is surfaced for reviewer confirmation, and **not covered** means the stack or pattern is intentionally out of scope for this release round. Narrative framework notes remain in [FRAMEWORK_COVERAGE.md](FRAMEWORK_COVERAGE.md) and provider/library notes remain in [PROVIDER_LIBRARY_COVERAGE.md](PROVIDER_LIBRARY_COVERAGE.md). @@ -30,6 +30,7 @@ Narrative framework notes remain in [FRAMEWORK_COVERAGE.md](FRAMEWORK_COVERAGE.m | `cookie_dynamic_path_scope` | JS/TS, Python | Express, Next.js, FastAPI, Django, generic Python | Cookie APIs | **review-required:** dynamic Path option | transmit | `dynamic_review_required` | `dynamic_review_required` | Runtime path scope requires review. | | `cookie_dynamic_expiry` | JS/TS, Python | Express, Next.js, FastAPI, Django, generic Python | Cookie APIs | **review-required:** dynamic Max-Age or Expires | expire | `dynamic_review_required` | `dynamic_review_required` | Runtime lifetime requires review. | | `cookie_missing_expiry` | JS/TS, Python | Express, Next.js, FastAPI, Django, generic Python | Cookie APIs | **supported:** session-like/signed cookie with no explicit expiry | expire | `lifecycle_gap` | `lifecycle_gap` | Explicit lifetime evidence is missing. | +| `sliding_expiry_without_rotation_review` | JS/TS, Python | Express, Django/session helpers, generic JS/TS/Python session or refresh helpers | Session middleware, session regeneration, refresh TTL helpers | **review-required:** rolling/sliding/idle TTL or Max-Age extension evidence without linked `session.regenerate`, `cycle_key`, session reissue, or refresh-token rotation; **not covered:** runtime-only framework settings outside scanned source | expire/refresh | `lifecycle_gap` | `lifecycle_gap` | Sliding expiry should be paired with session or refresh-token rotation evidence. | | `cookie_host_prefix_path_violation` | JS/TS, Python | Express, Next.js, FastAPI, Django runtime `set_cookie`, generic Python | Cookie APIs and static `Set-Cookie` | **supported:** literal `__Host-` name with Path missing or not `/`; **review-required:** dynamic Path | transmit | `high_confidence_misconfiguration` or `dynamic_review_required` | matching finding category | `__Host-` cookies must set `Path=/`. | | `cookie_host_prefix_domain_violation` | JS/TS, Python | Express, Next.js, FastAPI, Django runtime `set_cookie`, generic Python | Cookie APIs and static `Set-Cookie` | **supported:** literal `__Host-` name with any Domain; **review-required:** dynamic Domain | transmit | `high_confidence_misconfiguration` or `dynamic_review_required` | matching finding category | `__Host-` cookies must omit Domain and remain host-only. | | `cookie_host_prefix_secure_violation` | JS/TS, Python | Express, Next.js, FastAPI, Django runtime `set_cookie`, generic Python | Cookie APIs and static `Set-Cookie` | **supported:** literal `__Host-` name with missing/false Secure; **review-required:** dynamic/default Secure | transmit | `high_confidence_misconfiguration` or `dynamic_review_required` | matching finding category | `__Host-` cookies must be Secure. | @@ -45,6 +46,7 @@ Narrative framework notes remain in [FRAMEWORK_COVERAGE.md](FRAMEWORK_COVERAGE.m | `jwt_expiry_enforcement_disabled` | JS/TS, Python | Next.js, FastAPI, Django, generic JS/TS/Python | JWT verify APIs | **supported:** ignore/disable expiry checks | validate | `high_confidence_misconfiguration` | `high_confidence_misconfiguration` | Verification disables token expiry enforcement. | | `jwt_dynamic_expiry_enforcement` | JS/TS, Python | Next.js, FastAPI, Django, generic JS/TS/Python | JWT verify APIs | **review-required:** dynamic expiry enforcement | validate | `dynamic_review_required` | `dynamic_review_required` | Runtime verification options require review. | | `jwt_default_expiry_enforcement` | JS/TS, Python | Next.js, FastAPI, Django, generic JS/TS/Python | JWT verify APIs | **review-required:** library/default expiry enforcement | validate | `framework_default_assumed` | `framework_default_assumed` | Local source relies on library defaults. | +| `jwt_denylist_absent_on_logout_review` | JS/TS, Python | Express, Next.js, FastAPI, Django, generic JS/TS/Python | JWT issue/validate APIs plus logout handlers | **review-required:** access-JWT lifecycle evidence linked by source context to logout evidence with no visible denylist, blocklist, `revokeToken`, or revocation-store insertion; **not covered:** runtime-only short-TTL policy or external gateway revocation | revoke | `lifecycle_gap` | `lifecycle_gap` | Logout for stateless access JWTs needs reviewer confirmation when no denylist/revocation evidence is visible. | | `jwt_alg_none_accepted` | JS/TS, Python | Next.js, FastAPI, Django, generic JS/TS/Python | `jsonwebtoken`, `PyJWT`; `jose` explicit algorithm evidence | **supported:** literal `none` in `algorithms`/`algorithm`; **review-required:** missing allow-list on default-sensitive library paths; **not covered:** python-jose/authlib JWT validation paths | validate | `high_confidence_misconfiguration` or `framework_default_assumed` | matching finding category | JWT validation should not accept unsigned tokens and should pin allowed algorithms. | | `jwt_alg_confusion_signal` | JS/TS, Python | Next.js, FastAPI, Django, generic JS/TS/Python | `jsonwebtoken`, `jose`, `PyJWT` | **supported:** HMAC and asymmetric algorithms in one allow-list; **review-required:** HMAC algorithms paired with public-key-like key references; **not covered:** python-jose/authlib JWT validation paths | validate | `high_confidence_misconfiguration` or `dynamic_review_required` | matching finding category | Accepted algorithms should match the expected key family. | | `jwt_jku_header_trust` | JS/TS, Python | Next.js, FastAPI, Django, generic JS/TS/Python | `jsonwebtoken`, `jose`, `PyJWT` | **review-required:** `complete` verification context plus source-visible `header.jku` read; **not covered:** live URL allow-list verification or network JWKS state | validate | `dynamic_review_required` | `dynamic_review_required` | Header-driven key URLs require reviewer confirmation and allow-listing. | @@ -72,6 +74,8 @@ Narrative framework notes remain in [FRAMEWORK_COVERAGE.md](FRAMEWORK_COVERAGE.m | `bearer_missing_validation` | JS/TS, Python | Express, Next.js, FastAPI, Django, generic JS/TS/Python | Bearer/API-key handlers | **supported:** token use with no validation evidence | validate | `missing_validation_evidence` | `missing_validation_evidence` | Token validation evidence is missing. | | `bearer_issue_without_expiry` | JS/TS, Python | Generic JS/TS/Python | Bearer/API-key issue helpers | **supported:** issued opaque token without expiry | expire | `lifecycle_gap` | `lifecycle_gap` | Token issue flow lacks expiry evidence. | | `bearer_missing_rotation_or_revocation` | JS/TS, Python | Express, Next.js, FastAPI, Django, generic JS/TS/Python | Bearer/API-key lifecycle helpers | **supported:** refresh/reuse lifecycle without revoke/rotate evidence | revoke | `lifecycle_gap` | `lifecycle_gap` | Token lifecycle lacks rotation or revocation evidence. | +| `refresh_family_revocation_absent_on_logout_review` | JS/TS, Python | Express, Next.js, FastAPI, Django, generic JS/TS/Python | Refresh-token lifecycle helpers plus logout handlers | **review-required:** refresh-token lifecycle evidence linked by source context to logout evidence with no visible family/user-scoped refresh revocation such as `revokeRefreshFamily`, `delete refresh_tokens where user_id`, or refresh-family cache deletion; **not covered:** provider dashboard revocation outside source | revoke | `lifecycle_gap` | `lifecycle_gap` | Logout should revoke the user's refresh-token family, not only clear a browser cookie. | +| `password_change_global_revocation_absent_review` | JS/TS, Python | Express, Next.js, FastAPI, Django, generic JS/TS/Python | Password-change handlers plus session/refresh revocation helpers | **review-required:** password-change handler evidence without linked global session invalidation, refresh-family revocation, or token-version bump; **not covered:** standalone password validators or runtime-only identity-provider policy | revoke | `lifecycle_gap` | `lifecycle_gap` | Password changes should invalidate existing sessions and refresh families. | | `bearer_missing_scope_evidence` | JS/TS, Python | Generic JS/TS/Python, provider fixtures | Provider/token-boundary APIs | **supported:** token lacks scope/boundary evidence | validate | `missing_validation_evidence` | `missing_validation_evidence` | Scope or boundary evidence is missing. | | `bearer_dynamic_provider_review` | JS/TS, Python | Next.js, Express, generic JS/TS/Python | Auth0, Okta, Cognito, Azure AD, Firebase, Supabase, Clerk, Passport, NextAuth/AuthJS, OAuth/OIDC generic | **review-required:** provider-managed token behavior | refresh/revoke | `dynamic_review_required` | `dynamic_review_required` | Provider-managed lifecycle behavior needs human review. | | `query_param_token_acceptance_review` | JS/TS, Python | Express, Next.js, FastAPI, Django, generic JS/TS/Python | Request/query APIs | **review-required:** dynamic query token acceptance | transmit | `dynamic_review_required` | `dynamic_review_required` | Query token acceptance needs review. | diff --git a/docs/DESIGN_DECISIONS.md b/docs/DESIGN_DECISIONS.md index 7762117..646ffca 100644 --- a/docs/DESIGN_DECISIONS.md +++ b/docs/DESIGN_DECISIONS.md @@ -288,3 +288,73 @@ Rules: lifecycle management remain separate. - Missing or malformed baseline files are configuration errors whenever a baseline is explicitly supplied. + +## SS-DEC-011: v0.2 P1-P4 Check Category Consolidation + +The v0.2 edge-case hardening checks fit the existing five finding categories. +No new finding category or SARIF rule ID is added in this round, and the scan +report schema remains `0.5.0`. + +Explicit audit decisions: + +- **P2.3 `jwt_alg_confusion_signal`:** no new + `cryptographic_trust_violation` category. Literal mixed algorithm families + remain `high_confidence_misconfiguration`; key-family ambiguity remains + `dynamic_review_required`. +- **P2.4 JWT header-trust checks:** no new `cryptographic_trust_violation` + category. Header-driven `jku`, `x5u`, and embedded-JWK trust remains + `dynamic_review_required` because source evidence cannot prove live URL, + certificate, JWKS, or provider allow-list behavior. +- **P3.1 OAuth artifact type:** keep the dedicated `oauth_auth_code_flow` + artifact type. Reusing a token artifact would blur flow-construction evidence + with issued-token lifecycle evidence. + +Category mapping for new v0.2 checks: + +| Check ID | Category mapping | +| --- | --- | +| `cookie_host_prefix_path_violation` | `high_confidence_misconfiguration` for literal violations; `dynamic_review_required` for dynamic Path evidence | +| `cookie_host_prefix_domain_violation` | `high_confidence_misconfiguration` for literal Domain evidence; `dynamic_review_required` for dynamic Domain evidence | +| `cookie_host_prefix_secure_violation` | `high_confidence_misconfiguration` for missing/false literal Secure; `dynamic_review_required` for dynamic/default Secure evidence | +| `cookie_secure_prefix_secure_violation` | `high_confidence_misconfiguration` for missing/false literal Secure; `dynamic_review_required` for dynamic/default Secure evidence | +| `cookie_samesite_none_without_secure` | `high_confidence_misconfiguration` | +| `cookie_samesite_none_dynamic_secure` | `dynamic_review_required` | +| `cookie_samesite_none_default_secure` | `framework_default_assumed` | +| `cookie_samesite_none_cross_site_review` | `dynamic_review_required` | +| `cookie_partitioned_review` | `dynamic_review_required` | +| `cookie_domain_leak_review` | `dynamic_review_required` | +| `cookie_conflicting_writes_review` | `dynamic_review_required` | +| `jwt_alg_none_accepted` | `high_confidence_misconfiguration` for literal `none`; `framework_default_assumed` for default-sensitive library behavior | +| `jwt_alg_confusion_signal` | `high_confidence_misconfiguration` for deterministic mixed algorithm families; `dynamic_review_required` for key-family ambiguity | +| `jwt_jku_header_trust` | `dynamic_review_required` | +| `jwt_x5u_header_trust` | `dynamic_review_required` | +| `jwt_embedded_jwk_trust` | `dynamic_review_required` | +| `jwt_nbf_missing` | `missing_validation_evidence` | +| `jwt_clock_skew_review` | `dynamic_review_required` | +| `jwt_kid_unvalidated_review` | `missing_validation_evidence` | +| `oauth_pkce_missing_review` | `dynamic_review_required` | +| `oauth_state_missing` | `missing_validation_evidence` | +| `oauth_state_static_review` | `dynamic_review_required` | +| `oauth_state_unverified_review` | `missing_validation_evidence` | +| `oidc_nonce_missing` | `missing_validation_evidence` | +| `oidc_nonce_unverified_review` | `missing_validation_evidence` | +| `oauth_redirect_uri_wildcard_review` | `dynamic_review_required` | +| `token_in_local_storage` | `high_confidence_misconfiguration` | +| `token_in_session_storage` | `high_confidence_misconfiguration` | +| `token_in_url_path_or_fragment` | `high_confidence_misconfiguration` | +| `client_secret_in_browser_code` | `dynamic_review_required` | +| `jwt_denylist_absent_on_logout_review` | `lifecycle_gap` | +| `refresh_family_revocation_absent_on_logout_review` | `lifecycle_gap` | +| `sliding_expiry_without_rotation_review` | `lifecycle_gap` | +| `password_change_global_revocation_absent_review` | `lifecycle_gap` | + +Rules: + +- Prefer existing category semantics over adding near-duplicate category names. +- Keep cryptographic trust evidence in the category that describes the static + certainty: deterministic unsafe configuration, missing validation evidence, or + dynamic review. +- Keep SARIF stable by mapping findings through the existing category rule IDs. +- Revisit a dedicated cryptographic-trust category only if future checks cannot + be accurately expressed as deterministic misconfiguration, missing validation, + lifecycle gap, dynamic review, or framework default evidence. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 5fcd936..86ba8b7 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -74,3 +74,33 @@ cookies, claims, logout, and refresh while keeping findings evidence-bound. - Framework and provider coverage: #18 and #27 feed all four capability areas, with umbrella capability documentation completed in #40. - Focused command aliases: #39 exposes capability-oriented entry points while preserving the shared scan/config/reporting pipeline. - Stable CLI release: #28 tracks release packaging, versioning, installation workflow, and final readiness; #41 tracks this folded capability model without creating duplicate v1.1-v1.4 milestone tracks. + +## v0.2 edge-case hardening status + +The v0.2 depth-first edge-case hardening round is complete across all four +phases: + +- **P1 cookie prefix/attribute rules:** `__Host-` / `__Secure-`, + `SameSite=None` + Secure, Partitioned cookie review, broad non-session + Domain review, and conflicting same-handler cookie writes. +- **P2 JWT crypto-trust:** `alg:none`, algorithm-confusion signals, + `jku`/`x5u`/embedded-JWK header-trust review, missing `nbf`, broad + clock-skew review, and unvalidated `kid` review. +- **P3 OAuth/OIDC and client storage:** PKCE, `state`, OIDC `nonce`, wildcard + redirect URI review, browser storage token findings, URL path/fragment token + findings, browser-path client secret review, and OAuth redaction expansion. +- **P4 lifecycle and test hygiene:** JWT denylist-on-logout review, + refresh-family revocation-on-logout review, sliding-expiry-without-rotation + review, password-change global revocation review, clean-baseline + false-positive fixtures, JSON report snapshots, CLI exit-code matrix tests, + and the consolidated category audit. + +The consolidated category decision keeps the existing five finding categories +and does not require a schema or SARIF rule bump. + +## Deferred to v0.3+ + +New language and framework breadth is intentionally deferred. The next breadth +round may consider Flask, Tornado, Sanic, Starlette, NestJS, Koa, Fastify, Hapi, +Remix, Hono, SvelteKit, Go, Ruby/Rails, Java/Spring, .NET, PHP, python-jose, +authlib JWT validation paths, and deeper runtime/provider policy integration. diff --git a/docs/USAGE.md b/docs/USAGE.md index 641e582..a867a7c 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -129,8 +129,10 @@ SessionScope is built around defensive, evidence-bound checks. The current and p - OAuth/OIDC flow integrity, including `oauth_pkce_missing_review`, `oauth_state_missing`, `oauth_state_static_review`, `oauth_state_unverified_review`, `oidc_nonce_missing`, `oidc_nonce_unverified_review`, and `oauth_redirect_uri_wildcard_review` across Passport OAuth2, openid-client, NextAuth/Auth.js provider blocks, Authlib, and generic OAuth/OIDC code. - Client storage hygiene, including `token_in_local_storage`, `token_in_session_storage`, `token_in_url_path_or_fragment`, and `client_secret_in_browser_code` for browser-accessible storage and URL channels; `document.cookie` token writes are emitted as source evidence only in this phase. - Tokens issued without explicit expiry +- Sliding/idle expiry without linked rotation evidence, including `sliding_expiry_without_rotation_review` - Refresh tokens without rotation evidence -- Logout without revocation evidence +- Logout without revocation evidence, including `jwt_denylist_absent_on_logout_review` when access-JWT logout flows lack linked denylist/blocklist/revocation-store evidence and `refresh_family_revocation_absent_on_logout_review` when refresh-token logout flows lack family/user-scoped revocation evidence +- Password-change without global session or refresh-family revocation evidence, including `password_change_global_revocation_absent_review` - Password-reset tokens without expiry or single-use evidence - Session fixation risk signals - Token accepted from query parameters diff --git a/fixtures/django/clean-baseline-password-refresh/expected.json b/fixtures/django/clean-baseline-password-refresh/expected.json new file mode 100644 index 0000000..6faf68c --- /dev/null +++ b/fixtures/django/clean-baseline-password-refresh/expected.json @@ -0,0 +1,11 @@ +{ + "fixture_id": "django.clean-baseline-password-refresh", + "framework": "django", + "source_files": [ + "views.py" + ], + "expected_artifacts": [], + "expected_lifecycle_stages": [], + "expected_findings": [], + "notes": "Clean Django baseline with no password-change or refresh findings." +} diff --git a/fixtures/django/clean-baseline-password-refresh/views.py b/fixtures/django/clean-baseline-password-refresh/views.py new file mode 100644 index 0000000..70eddd2 --- /dev/null +++ b/fixtures/django/clean-baseline-password-refresh/views.py @@ -0,0 +1,2 @@ +def health_view(request): + return {"ok": True} diff --git a/fixtures/django/password-change-current-session-only/expected.json b/fixtures/django/password-change-current-session-only/expected.json new file mode 100644 index 0000000..dadbd00 --- /dev/null +++ b/fixtures/django/password-change-current-session-only/expected.json @@ -0,0 +1,9 @@ +{ + "fixture_id": "django-password-change-current-session-only", + "framework": "django", + "source_files": ["views.py"], + "expected_artifacts": ["password_change", "session"], + "expected_lifecycle_stages": ["revoke", "refresh"], + "expected_findings": ["password_change_global_revocation_absent_review"], + "notes": "Password-change handlers that only rotate or destroy the current session still need review because global session invalidation, refresh-family revocation, or token-version bump evidence is absent." +} diff --git a/fixtures/django/password-change-current-session-only/views.py b/fixtures/django/password-change-current-session-only/views.py new file mode 100644 index 0000000..46e82c1 --- /dev/null +++ b/fixtures/django/password-change-current-session-only/views.py @@ -0,0 +1,6 @@ +def password_change_complete(request): + request.user.set_password(request.POST["new_password"]) + request.user.save() + request.session.cycle_key() + logout(request) + return {"ok": True} diff --git a/fixtures/django/password-change-global-revoke/expected.json b/fixtures/django/password-change-global-revoke/expected.json new file mode 100644 index 0000000..918fa8f --- /dev/null +++ b/fixtures/django/password-change-global-revoke/expected.json @@ -0,0 +1,9 @@ +{ + "fixture_id": "django.password-change-global-revoke", + "framework": "django", + "source_files": ["views.py"], + "expected_artifacts": ["password_change", "session"], + "expected_lifecycle_stages": ["revoke"], + "expected_findings": ["password_change_global_revocation_evidence_present"], + "notes": "Covers a password-change handler with visible global session invalidation and token-version bump evidence." +} diff --git a/fixtures/django/password-change-global-revoke/views.py b/fixtures/django/password-change-global-revoke/views.py new file mode 100644 index 0000000..4458b63 --- /dev/null +++ b/fixtures/django/password-change-global-revoke/views.py @@ -0,0 +1,6 @@ +def password_change_complete(request): + request.user.set_password(request.POST["new_password"]) + request.user.save() + revoke_all_sessions(request.user.pk) + bump_token_version(request.user.pk) + return {"ok": True} diff --git a/fixtures/django/password-change-without-global-revoke/expected.json b/fixtures/django/password-change-without-global-revoke/expected.json new file mode 100644 index 0000000..9a36030 --- /dev/null +++ b/fixtures/django/password-change-without-global-revoke/expected.json @@ -0,0 +1,9 @@ +{ + "fixture_id": "django.password-change-without-global-revoke", + "framework": "django", + "source_files": ["views.py"], + "expected_artifacts": ["password_change"], + "expected_lifecycle_stages": ["revoke"], + "expected_findings": ["password_change_global_revocation_absent_review"], + "notes": "Covers a password-change handler that updates the password hash without global session, refresh-family, or token-version revocation evidence." +} diff --git a/fixtures/django/password-change-without-global-revoke/views.py b/fixtures/django/password-change-without-global-revoke/views.py new file mode 100644 index 0000000..7fd02cc --- /dev/null +++ b/fixtures/django/password-change-without-global-revoke/views.py @@ -0,0 +1,4 @@ +def password_change_complete(request): + request.user.set_password(request.POST["new_password"]) + request.user.save() + return {"ok": True} diff --git a/fixtures/express/clean-baseline-lifecycle/app.ts b/fixtures/express/clean-baseline-lifecycle/app.ts new file mode 100644 index 0000000..d274a34 --- /dev/null +++ b/fixtures/express/clean-baseline-lifecycle/app.ts @@ -0,0 +1,7 @@ +import express from "express"; + +const app = express(); + +app.get("/health", (_request, response) => { + response.json({ ok: true }); +}); diff --git a/fixtures/express/clean-baseline-lifecycle/expected.json b/fixtures/express/clean-baseline-lifecycle/expected.json new file mode 100644 index 0000000..001a214 --- /dev/null +++ b/fixtures/express/clean-baseline-lifecycle/expected.json @@ -0,0 +1,11 @@ +{ + "fixture_id": "express.clean-baseline-lifecycle", + "framework": "express", + "source_files": [ + "app.ts" + ], + "expected_artifacts": [], + "expected_lifecycle_stages": [], + "expected_findings": [], + "notes": "Clean Express baseline with no token/session lifecycle findings." +} diff --git a/fixtures/express/fixed-expiry-session/app.ts b/fixtures/express/fixed-expiry-session/app.ts new file mode 100644 index 0000000..4dde900 --- /dev/null +++ b/fixtures/express/fixed-expiry-session/app.ts @@ -0,0 +1,15 @@ +import express from "express"; +import session from "express-session"; + +const app = express(); + +app.use(session({ + secret: "PLACEHOLDER_SECRET_DO_NOT_USE", + resave: false, + saveUninitialized: false, + cookie: { httpOnly: true, secure: true, maxAge: 900000 }, +})); + +app.get("/account", (_request, response) => { + response.json({ ok: true }); +}); diff --git a/fixtures/express/fixed-expiry-session/expected.json b/fixtures/express/fixed-expiry-session/expected.json new file mode 100644 index 0000000..cc51ea2 --- /dev/null +++ b/fixtures/express/fixed-expiry-session/expected.json @@ -0,0 +1,9 @@ +{ + "fixture_id": "express.fixed-expiry-session", + "framework": "express", + "source_files": ["app.ts"], + "expected_artifacts": ["session"], + "expected_lifecycle_stages": ["store"], + "expected_findings": ["fixed_expiry_session_no_sliding_review"], + "notes": "Covers fixed-expiry Express session middleware that should not trigger sliding-expiry review." +} diff --git a/fixtures/express/logout-refresh-family-missing/app.ts b/fixtures/express/logout-refresh-family-missing/app.ts new file mode 100644 index 0000000..8a8a828 --- /dev/null +++ b/fixtures/express/logout-refresh-family-missing/app.ts @@ -0,0 +1,18 @@ +import express from "express"; + +const app = express(); + +async function issueRefreshToken(userId: string) { + return refreshTokens.create({ userId, expiresAt: Date.now() + 86400 }); +} + +app.post("/refresh", async (request, response) => { + const refreshToken = request.cookies.refresh_token; + const nextToken = await issueRefreshToken(request.user.id); + response.cookie("refresh_token", nextToken, { httpOnly: true, maxAge: 86400 }); +}); + +app.post("/logout", (_request, response) => { + response.clearCookie("refresh_token"); + response.status(204).end(); +}); diff --git a/fixtures/express/logout-refresh-family-missing/expected.json b/fixtures/express/logout-refresh-family-missing/expected.json new file mode 100644 index 0000000..5408e26 --- /dev/null +++ b/fixtures/express/logout-refresh-family-missing/expected.json @@ -0,0 +1,9 @@ +{ + "fixture_id": "express.logout-refresh-family-missing", + "framework": "express", + "source_files": ["app.ts"], + "expected_artifacts": ["refresh_token"], + "expected_lifecycle_stages": ["issue", "store", "refresh", "revoke", "expire"], + "expected_findings": ["refresh_family_revocation_absent_on_logout_review"], + "notes": "Covers logout that clears the refresh cookie while leaving the server-side refresh-token family active." +} diff --git a/fixtures/express/logout-refresh-family-revoked/app.ts b/fixtures/express/logout-refresh-family-revoked/app.ts new file mode 100644 index 0000000..cfb2bd7 --- /dev/null +++ b/fixtures/express/logout-refresh-family-revoked/app.ts @@ -0,0 +1,23 @@ +import express from "express"; + +const app = express(); + +async function issueRefreshToken(userId: string) { + return refreshTokens.create({ userId, expiresAt: Date.now() + 86400 }); +} + +async function revokeRefreshFamily(userId: string) { + await refreshTokens.deleteMany({ user_id: userId }); +} + +app.post("/refresh", async (request, response) => { + const refreshToken = request.cookies.refresh_token; + const nextToken = await issueRefreshToken(request.user.id); + response.cookie("refresh_token", nextToken, { httpOnly: true, maxAge: 86400 }); +}); + +app.post("/logout", async (request, response) => { + await revokeRefreshFamily(request.user.id); + response.clearCookie("refresh_token"); + response.status(204).end(); +}); diff --git a/fixtures/express/logout-refresh-family-revoked/expected.json b/fixtures/express/logout-refresh-family-revoked/expected.json new file mode 100644 index 0000000..6caa589 --- /dev/null +++ b/fixtures/express/logout-refresh-family-revoked/expected.json @@ -0,0 +1,9 @@ +{ + "fixture_id": "express.logout-refresh-family-revoked", + "framework": "express", + "source_files": ["app.ts"], + "expected_artifacts": ["refresh_token"], + "expected_lifecycle_stages": ["issue", "store", "refresh", "revoke", "expire"], + "expected_findings": ["refresh_family_revocation_evidence_present"], + "notes": "Covers logout that calls a user-scoped refresh-family revocation helper." +} diff --git a/fixtures/express/sliding-expiry-with-rotation/app.ts b/fixtures/express/sliding-expiry-with-rotation/app.ts new file mode 100644 index 0000000..dacbdd6 --- /dev/null +++ b/fixtures/express/sliding-expiry-with-rotation/app.ts @@ -0,0 +1,19 @@ +import express from "express"; +import session from "express-session"; + +const app = express(); + +app.use(session({ + secret: "PLACEHOLDER_SECRET_DO_NOT_USE", + resave: true, + saveUninitialized: false, + rolling: true, + cookie: { httpOnly: true, secure: true, maxAge: 900000 }, +})); + +app.post("/login", (request, response) => { + request.session.regenerate(() => { + request.session.userId = "placeholder-user"; + response.json({ ok: true }); + }); +}); diff --git a/fixtures/express/sliding-expiry-with-rotation/expected.json b/fixtures/express/sliding-expiry-with-rotation/expected.json new file mode 100644 index 0000000..517bd1f --- /dev/null +++ b/fixtures/express/sliding-expiry-with-rotation/expected.json @@ -0,0 +1,9 @@ +{ + "fixture_id": "express.sliding-expiry-with-rotation", + "framework": "express", + "source_files": ["app.ts"], + "expected_artifacts": ["session"], + "expected_lifecycle_stages": ["store", "refresh"], + "expected_findings": ["sliding_expiry_rotation_evidence_present"], + "notes": "Covers rolling session middleware with visible session regeneration evidence." +} diff --git a/fixtures/express/sliding-expiry-without-rotation/app.ts b/fixtures/express/sliding-expiry-without-rotation/app.ts new file mode 100644 index 0000000..0beda26 --- /dev/null +++ b/fixtures/express/sliding-expiry-without-rotation/app.ts @@ -0,0 +1,16 @@ +import express from "express"; +import session from "express-session"; + +const app = express(); + +app.use(session({ + secret: "PLACEHOLDER_SECRET_DO_NOT_USE", + resave: true, + saveUninitialized: false, + rolling: true, + cookie: { httpOnly: true, secure: true, maxAge: 900000 }, +})); + +app.get("/account", (request, response) => { + response.json({ ok: true }); +}); diff --git a/fixtures/express/sliding-expiry-without-rotation/expected.json b/fixtures/express/sliding-expiry-without-rotation/expected.json new file mode 100644 index 0000000..bc5939d --- /dev/null +++ b/fixtures/express/sliding-expiry-without-rotation/expected.json @@ -0,0 +1,9 @@ +{ + "fixture_id": "express.sliding-expiry-without-rotation", + "framework": "express", + "source_files": ["app.ts"], + "expected_artifacts": ["session"], + "expected_lifecycle_stages": ["store"], + "expected_findings": ["sliding_expiry_without_rotation_review"], + "notes": "Covers Express rolling session middleware that extends Max-Age on each request without visible session rotation evidence." +} diff --git a/fixtures/fastapi/clean-baseline-lifecycle/app.py b/fixtures/fastapi/clean-baseline-lifecycle/app.py new file mode 100644 index 0000000..1887d14 --- /dev/null +++ b/fixtures/fastapi/clean-baseline-lifecycle/app.py @@ -0,0 +1,7 @@ +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/health") +def health(): + return {"ok": True} diff --git a/fixtures/fastapi/clean-baseline-lifecycle/expected.json b/fixtures/fastapi/clean-baseline-lifecycle/expected.json new file mode 100644 index 0000000..d69ff16 --- /dev/null +++ b/fixtures/fastapi/clean-baseline-lifecycle/expected.json @@ -0,0 +1,11 @@ +{ + "fixture_id": "fastapi.clean-baseline-lifecycle", + "framework": "fastapi", + "source_files": [ + "app.py" + ], + "expected_artifacts": [], + "expected_lifecycle_stages": [], + "expected_findings": [], + "notes": "Clean FastAPI baseline with no token/session lifecycle findings." +} diff --git a/fixtures/generic-js/clean-baseline-jwt/app.js b/fixtures/generic-js/clean-baseline-jwt/app.js new file mode 100644 index 0000000..12cf541 --- /dev/null +++ b/fixtures/generic-js/clean-baseline-jwt/app.js @@ -0,0 +1,3 @@ +export function buildGreeting(name) { + return `hello ${name}`; +} diff --git a/fixtures/generic-js/clean-baseline-jwt/expected.json b/fixtures/generic-js/clean-baseline-jwt/expected.json new file mode 100644 index 0000000..163221e --- /dev/null +++ b/fixtures/generic-js/clean-baseline-jwt/expected.json @@ -0,0 +1,11 @@ +{ + "fixture_id": "generic-js.clean-baseline-jwt", + "framework": "generic-javascript", + "source_files": [ + "app.js" + ], + "expected_artifacts": [], + "expected_lifecycle_stages": [], + "expected_findings": [], + "notes": "Clean JavaScript baseline with no JWT or client-storage findings." +} diff --git a/fixtures/generic-python/clean-baseline-jwt-refresh/app.py b/fixtures/generic-python/clean-baseline-jwt-refresh/app.py new file mode 100644 index 0000000..5449413 --- /dev/null +++ b/fixtures/generic-python/clean-baseline-jwt-refresh/app.py @@ -0,0 +1,2 @@ +def build_greeting(name: str) -> str: + return f"hello {name}" diff --git a/fixtures/generic-python/clean-baseline-jwt-refresh/expected.json b/fixtures/generic-python/clean-baseline-jwt-refresh/expected.json new file mode 100644 index 0000000..481a73f --- /dev/null +++ b/fixtures/generic-python/clean-baseline-jwt-refresh/expected.json @@ -0,0 +1,11 @@ +{ + "fixture_id": "generic-python.clean-baseline-jwt-refresh", + "framework": "generic-python", + "source_files": [ + "app.py" + ], + "expected_artifacts": [], + "expected_lifecycle_stages": [], + "expected_findings": [], + "notes": "Clean Python baseline with no JWT, refresh, or password-change findings." +} diff --git a/fixtures/generic-python/password-validation-utility/app.py b/fixtures/generic-python/password-validation-utility/app.py new file mode 100644 index 0000000..12aaa0c --- /dev/null +++ b/fixtures/generic-python/password-validation-utility/app.py @@ -0,0 +1,2 @@ +def validate_password_strength(candidate: str) -> bool: + return len(candidate) >= 12 and any(ch.isdigit() for ch in candidate) diff --git a/fixtures/generic-python/password-validation-utility/expected.json b/fixtures/generic-python/password-validation-utility/expected.json new file mode 100644 index 0000000..744767e --- /dev/null +++ b/fixtures/generic-python/password-validation-utility/expected.json @@ -0,0 +1,9 @@ +{ + "fixture_id": "generic-python.password-validation-utility", + "framework": "generic-python", + "source_files": ["app.py"], + "expected_artifacts": [], + "expected_lifecycle_stages": [], + "expected_findings": ["password_validation_utility_no_password_change_review"], + "notes": "Covers a standalone password-validation helper that should not be treated as a password-change handler." +} diff --git a/fixtures/generic-ts/clean-baseline-jwt-oauth/app.ts b/fixtures/generic-ts/clean-baseline-jwt-oauth/app.ts new file mode 100644 index 0000000..16e8ab9 --- /dev/null +++ b/fixtures/generic-ts/clean-baseline-jwt-oauth/app.ts @@ -0,0 +1,3 @@ +export function buildGreeting(name: string): string { + return `hello ${name}`; +} diff --git a/fixtures/generic-ts/clean-baseline-jwt-oauth/expected.json b/fixtures/generic-ts/clean-baseline-jwt-oauth/expected.json new file mode 100644 index 0000000..5cd3ce6 --- /dev/null +++ b/fixtures/generic-ts/clean-baseline-jwt-oauth/expected.json @@ -0,0 +1,11 @@ +{ + "fixture_id": "generic-ts.clean-baseline-jwt-oauth", + "framework": "generic-typescript", + "source_files": [ + "app.ts" + ], + "expected_artifacts": [], + "expected_lifecycle_stages": [], + "expected_findings": [], + "notes": "Clean TypeScript baseline with no JWT, OAuth, or lifecycle findings." +} diff --git a/fixtures/generic-ts/jwt-logout-with-denylist/app.ts b/fixtures/generic-ts/jwt-logout-with-denylist/app.ts new file mode 100644 index 0000000..4323e37 --- /dev/null +++ b/fixtures/generic-ts/jwt-logout-with-denylist/app.ts @@ -0,0 +1,21 @@ +import express from "express"; +import jwt from "jsonwebtoken"; + +const app = express(); +const JWT_SECRET = "PLACEHOLDER_SECRET_DO_NOT_USE"; +const revokedTokens = new Set(); + +export function issueAccessJwt(userId: string): string { + return jwt.sign({ sub: userId }, JWT_SECRET, { expiresIn: "15m", jwtid: "placeholder-jti" }); +} + +function revokeToken(accessToken: string) { + revokedTokens.add(accessToken); +} + +app.post("/logout", (request, response) => { + const accessToken = request.header("authorization") ?? ""; + revokeToken(accessToken); + response.clearCookie("session"); + response.status(204).end(); +}); diff --git a/fixtures/generic-ts/jwt-logout-with-denylist/expected.json b/fixtures/generic-ts/jwt-logout-with-denylist/expected.json new file mode 100644 index 0000000..c982fa7 --- /dev/null +++ b/fixtures/generic-ts/jwt-logout-with-denylist/expected.json @@ -0,0 +1,9 @@ +{ + "fixture_id": "generic-ts.jwt-logout-with-denylist", + "framework": "generic-typescript", + "source_files": ["app.ts"], + "expected_artifacts": ["access_jwt", "session"], + "expected_lifecycle_stages": ["issue", "revoke", "expire"], + "expected_findings": ["jwt_denylist_evidence_present"], + "notes": "Covers an access-JWT logout path with source-visible token revocation evidence so the denylist-absent review should not fire." +} diff --git a/fixtures/generic-ts/jwt-logout-without-denylist/app.ts b/fixtures/generic-ts/jwt-logout-without-denylist/app.ts new file mode 100644 index 0000000..e57652e --- /dev/null +++ b/fixtures/generic-ts/jwt-logout-without-denylist/app.ts @@ -0,0 +1,14 @@ +import express from "express"; +import jwt from "jsonwebtoken"; + +const app = express(); +const JWT_SECRET = "PLACEHOLDER_SECRET_DO_NOT_USE"; + +export function issueAccessJwt(userId: string): string { + return jwt.sign({ sub: userId }, JWT_SECRET, { expiresIn: "15m" }); +} + +app.post("/logout", (_request, response) => { + response.clearCookie("session"); + response.status(204).end(); +}); diff --git a/fixtures/generic-ts/jwt-logout-without-denylist/expected.json b/fixtures/generic-ts/jwt-logout-without-denylist/expected.json new file mode 100644 index 0000000..8860de0 --- /dev/null +++ b/fixtures/generic-ts/jwt-logout-without-denylist/expected.json @@ -0,0 +1,9 @@ +{ + "fixture_id": "generic-ts.jwt-logout-without-denylist", + "framework": "generic-typescript", + "source_files": ["app.ts"], + "expected_artifacts": ["access_jwt", "session"], + "expected_lifecycle_stages": ["issue", "revoke", "expire"], + "expected_findings": ["jwt_denylist_absent_on_logout_review"], + "notes": "Covers an access-JWT issuing app with logout evidence but no linked denylist, blocklist, or token revocation-store insertion." +} diff --git a/fixtures/nextjs/clean-baseline-oauth-storage/expected.json b/fixtures/nextjs/clean-baseline-oauth-storage/expected.json new file mode 100644 index 0000000..f718d2d --- /dev/null +++ b/fixtures/nextjs/clean-baseline-oauth-storage/expected.json @@ -0,0 +1,11 @@ +{ + "fixture_id": "nextjs.clean-baseline-oauth-storage", + "framework": "nextjs", + "source_files": [ + "route.ts" + ], + "expected_artifacts": [], + "expected_lifecycle_stages": [], + "expected_findings": [], + "notes": "Clean Next.js baseline with no OAuth or client-storage findings." +} diff --git a/fixtures/nextjs/clean-baseline-oauth-storage/route.ts b/fixtures/nextjs/clean-baseline-oauth-storage/route.ts new file mode 100644 index 0000000..9146308 --- /dev/null +++ b/fixtures/nextjs/clean-baseline-oauth-storage/route.ts @@ -0,0 +1,3 @@ +export async function GET() { + return Response.json({ ok: true }); +} diff --git a/tests/README.md b/tests/README.md index 3c50fb9..4f17c2e 100644 --- a/tests/README.md +++ b/tests/README.md @@ -4,3 +4,28 @@ Workspace-level integration scenarios should live here when they need to scan fixture repositories or compare multiple output formats. Crate-local unit and CLI tests should stay next to their crate when possible. +## False-positive fixture contract + +Clean baseline fixtures live under `fixtures/*/clean-baseline-*`. Their +`expected_findings` arrays are intentionally empty, and the fixture harness +asserts that scanning each clean baseline produces no findings. These fixtures +provide the cumulative false-positive guard for every v0.2 P1-P4 check ID. + +## JSON snapshots + +Canonical JSON report snapshots live in `tests/integration/snapshots/` and are +checked by the hand-rolled `sessionscope-testing` integration test. Regenerate +them after intentional report-output changes with: + +```bash +SESSIONSCOPE_UPDATE_JSON_SNAPSHOTS=1 cargo test -p sessionscope-testing --test json_snapshots +``` + +## CLI exit-code matrix + +`crates/sessionscope-cli/tests/cli_exit_matrix.rs` is the Rust integration-test +equivalent of the documented exit-code shell matrix. It evaluates synthetic JSON +reports through the real `sessionscope evaluate` binary path and covers advisory +mode, enforce thresholds, category filters, include/exclude finding IDs, and +baseline suppression/precedence. + diff --git a/tests/integration/snapshots/django-session-and-reset-flow.json b/tests/integration/snapshots/django-session-and-reset-flow.json new file mode 100644 index 0000000..4732d73 --- /dev/null +++ b/tests/integration/snapshots/django-session-and-reset-flow.json @@ -0,0 +1,900 @@ +{ + "schema_version": "0.5.0", + "summary": { + "files_discovered": 3, + "files_scanned": 3, + "files_skipped": 0, + "diagnostics": [], + "worker_panic_count": 0 + }, + "files": [ + { + "path": "expected.json", + "language": "json", + "artifacts": [], + "evidence": [], + "diagnostics": [], + "skipped_reason": null + }, + { + "path": "settings.py", + "language": "python", + "artifacts": [ + { + "id": "artifact_99ec026c78bdccbb", + "artifact_type": "session_cookie", + "display_name": "sessionid", + "locations": [ + { + "path": "settings.py", + "line": 2, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_99ec026c78bdccbb", + "evidence_ca1fe3d007a72505", + "evidence_ec9bef1d37b762fd" + ], + "transmit": [ + "evidence_1c3527e01b7878d2", + "evidence_1ec1adbc2e3731f8", + "evidence_b696d1f40628568a", + "evidence_c6eb4d0f4c5683ab" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_054287c83ffbc9d9", + "evidence_a720310d31ee0f13" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "django" + ], + "cookie_attributes": { + "http_only": { + "state": "present", + "value": "True", + "evidence_ids": [ + "evidence_ec9bef1d37b762fd" + ], + "confidence": "high" + }, + "secure": { + "state": "present", + "value": "True", + "evidence_ids": [ + "evidence_1c3527e01b7878d2" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "Lax", + "evidence_ids": [ + "evidence_1ec1adbc2e3731f8" + ], + "confidence": "high" + }, + "max_age": { + "state": "framework_default", + "value": "1209600", + "evidence_ids": [ + "evidence_054287c83ffbc9d9" + ], + "confidence": "low" + }, + "expires": { + "state": "framework_default", + "value": "none", + "evidence_ids": [ + "evidence_a720310d31ee0f13" + ], + "confidence": "low" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_b696d1f40628568a" + ], + "confidence": "low" + }, + "domain": { + "state": "framework_default", + "value": "none", + "evidence_ids": [ + "evidence_c6eb4d0f4c5683ab" + ], + "confidence": "low" + } + } + } + ], + "evidence": [ + { + "id": "evidence_ca1fe3d007a72505", + "lifecycle_stage": "store", + "location": { + "path": "settings.py", + "line": 2, + "column": 1 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_99ec026c78bdccbb", + "lifecycle_stage": "store", + "location": { + "path": "settings.py", + "line": 2, + "column": 1 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": "Django SESSION_COOKIE_* settings", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c6eb4d0f4c5683ab", + "lifecycle_stage": "transmit", + "location": { + "path": "settings.py", + "line": 2, + "column": 1 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "low", + "excerpt": "Domain defaults to none for django", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_b696d1f40628568a", + "lifecycle_stage": "transmit", + "location": { + "path": "settings.py", + "line": 2, + "column": 1 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for django", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_a720310d31ee0f13", + "lifecycle_stage": "expire", + "location": { + "path": "settings.py", + "line": 2, + "column": 1 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "low", + "excerpt": "Expires defaults to none for django", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_054287c83ffbc9d9", + "lifecycle_stage": "expire", + "location": { + "path": "settings.py", + "line": 2, + "column": 1 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "low", + "excerpt": "Max-Age defaults to 1209600 for django", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_ec9bef1d37b762fd", + "lifecycle_stage": "store", + "location": { + "path": "settings.py", + "line": 2, + "column": 27 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "SESSION_COOKIE_HTTPONLY = True", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1c3527e01b7878d2", + "lifecycle_stage": "transmit", + "location": { + "path": "settings.py", + "line": 3, + "column": 25 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "SESSION_COOKIE_SECURE = True", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1ec1adbc2e3731f8", + "lifecycle_stage": "transmit", + "location": { + "path": "settings.py", + "line": 4, + "column": 27 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SESSION_COOKIE_SAMESITE = \"Lax\"", + "dynamic": false, + "framework_default": false + } + ], + "diagnostics": [], + "skipped_reason": null + }, + { + "path": "views.py", + "language": "python", + "artifacts": [ + { + "id": "artifact_2d4eceb282cb58d6", + "artifact_type": "unknown", + "display_name": "logout", + "locations": [ + { + "path": "views.py", + "line": 18, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_4e76a3048eeb65c6" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "python" + ] + }, + { + "id": "artifact_4dc9e49ac137a92c", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "views.py", + "line": 19, + "column": 5 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_08fb01f27b9526e6" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "python" + ] + }, + { + "id": "artifact_c807d79fde47d60c", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "views.py", + "line": 20, + "column": 5 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_46e713f862bd7a7a" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "django" + ] + }, + { + "id": "artifact_e34a318309f55157", + "artifact_type": "session_cookie", + "display_name": "sessionid", + "locations": [ + { + "path": "views.py", + "line": 22, + "column": 5 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_9d5c6bbd4b320590" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "python" + ] + } + ], + "evidence": [ + { + "id": "evidence_4e76a3048eeb65c6", + "lifecycle_stage": "revoke", + "location": { + "path": "views.py", + "line": 18, + "column": 1 + }, + "detector_id": "logout.handler", + "confidence": "high", + "excerpt": "def complete_logout(request):\n revoke_user_sessions(request.user.pk)\n logout(request)\n response = HttpResponse(status=204)\n response.delete_cookie(\"[REDACTED]\")\n return response", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_08fb01f27b9526e6", + "lifecycle_stage": "revoke", + "location": { + "path": "views.py", + "line": 19, + "column": 5 + }, + "detector_id": "logout.session_destroy", + "confidence": "high", + "excerpt": "revoke_user_sessions(request.user.pk)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_46e713f862bd7a7a", + "lifecycle_stage": "revoke", + "location": { + "path": "views.py", + "line": 20, + "column": 5 + }, + "detector_id": "logout.session_destroy", + "confidence": "high", + "excerpt": "logout(request)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9d5c6bbd4b320590", + "lifecycle_stage": "revoke", + "location": { + "path": "views.py", + "line": 22, + "column": 5 + }, + "detector_id": "logout.cookie_clear", + "confidence": "high", + "excerpt": "response.delete_cookie(\"sessionid\")", + "dynamic": false, + "framework_default": false + } + ], + "diagnostics": [], + "skipped_reason": null + } + ], + "artifacts": [ + { + "id": "artifact_99ec026c78bdccbb", + "artifact_type": "session_cookie", + "display_name": "sessionid", + "locations": [ + { + "path": "settings.py", + "line": 2, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_99ec026c78bdccbb", + "evidence_ca1fe3d007a72505", + "evidence_ec9bef1d37b762fd" + ], + "transmit": [ + "evidence_1c3527e01b7878d2", + "evidence_1ec1adbc2e3731f8", + "evidence_b696d1f40628568a", + "evidence_c6eb4d0f4c5683ab" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_054287c83ffbc9d9", + "evidence_a720310d31ee0f13" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "django" + ], + "cookie_attributes": { + "http_only": { + "state": "present", + "value": "True", + "evidence_ids": [ + "evidence_ec9bef1d37b762fd" + ], + "confidence": "high" + }, + "secure": { + "state": "present", + "value": "True", + "evidence_ids": [ + "evidence_1c3527e01b7878d2" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "Lax", + "evidence_ids": [ + "evidence_1ec1adbc2e3731f8" + ], + "confidence": "high" + }, + "max_age": { + "state": "framework_default", + "value": "1209600", + "evidence_ids": [ + "evidence_054287c83ffbc9d9" + ], + "confidence": "low" + }, + "expires": { + "state": "framework_default", + "value": "none", + "evidence_ids": [ + "evidence_a720310d31ee0f13" + ], + "confidence": "low" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_b696d1f40628568a" + ], + "confidence": "low" + }, + "domain": { + "state": "framework_default", + "value": "none", + "evidence_ids": [ + "evidence_c6eb4d0f4c5683ab" + ], + "confidence": "low" + } + } + }, + { + "id": "artifact_2d4eceb282cb58d6", + "artifact_type": "unknown", + "display_name": "logout", + "locations": [ + { + "path": "views.py", + "line": 18, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_4e76a3048eeb65c6" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "python" + ] + }, + { + "id": "artifact_4dc9e49ac137a92c", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "views.py", + "line": 19, + "column": 5 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_08fb01f27b9526e6" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "python" + ] + }, + { + "id": "artifact_c807d79fde47d60c", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "views.py", + "line": 20, + "column": 5 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_46e713f862bd7a7a" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "django" + ] + }, + { + "id": "artifact_e34a318309f55157", + "artifact_type": "session_cookie", + "display_name": "sessionid", + "locations": [ + { + "path": "views.py", + "line": 22, + "column": 5 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_9d5c6bbd4b320590" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "python" + ] + } + ], + "evidence": [ + { + "id": "evidence_ca1fe3d007a72505", + "lifecycle_stage": "store", + "location": { + "path": "settings.py", + "line": 2, + "column": 1 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_99ec026c78bdccbb", + "lifecycle_stage": "store", + "location": { + "path": "settings.py", + "line": 2, + "column": 1 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": "Django SESSION_COOKIE_* settings", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c6eb4d0f4c5683ab", + "lifecycle_stage": "transmit", + "location": { + "path": "settings.py", + "line": 2, + "column": 1 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "low", + "excerpt": "Domain defaults to none for django", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_b696d1f40628568a", + "lifecycle_stage": "transmit", + "location": { + "path": "settings.py", + "line": 2, + "column": 1 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for django", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_a720310d31ee0f13", + "lifecycle_stage": "expire", + "location": { + "path": "settings.py", + "line": 2, + "column": 1 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "low", + "excerpt": "Expires defaults to none for django", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_054287c83ffbc9d9", + "lifecycle_stage": "expire", + "location": { + "path": "settings.py", + "line": 2, + "column": 1 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "low", + "excerpt": "Max-Age defaults to 1209600 for django", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_ec9bef1d37b762fd", + "lifecycle_stage": "store", + "location": { + "path": "settings.py", + "line": 2, + "column": 27 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "SESSION_COOKIE_HTTPONLY = True", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1c3527e01b7878d2", + "lifecycle_stage": "transmit", + "location": { + "path": "settings.py", + "line": 3, + "column": 25 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "SESSION_COOKIE_SECURE = True", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1ec1adbc2e3731f8", + "lifecycle_stage": "transmit", + "location": { + "path": "settings.py", + "line": 4, + "column": 27 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SESSION_COOKIE_SAMESITE = \"Lax\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_4e76a3048eeb65c6", + "lifecycle_stage": "revoke", + "location": { + "path": "views.py", + "line": 18, + "column": 1 + }, + "detector_id": "logout.handler", + "confidence": "high", + "excerpt": "def complete_logout(request):\n revoke_user_sessions(request.user.pk)\n logout(request)\n response = HttpResponse(status=204)\n response.delete_cookie(\"[REDACTED]\")\n return response", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_08fb01f27b9526e6", + "lifecycle_stage": "revoke", + "location": { + "path": "views.py", + "line": 19, + "column": 5 + }, + "detector_id": "logout.session_destroy", + "confidence": "high", + "excerpt": "revoke_user_sessions(request.user.pk)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_46e713f862bd7a7a", + "lifecycle_stage": "revoke", + "location": { + "path": "views.py", + "line": 20, + "column": 5 + }, + "detector_id": "logout.session_destroy", + "confidence": "high", + "excerpt": "logout(request)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9d5c6bbd4b320590", + "lifecycle_stage": "revoke", + "location": { + "path": "views.py", + "line": 22, + "column": 5 + }, + "detector_id": "logout.cookie_clear", + "confidence": "high", + "excerpt": "response.delete_cookie(\"sessionid\")", + "dynamic": false, + "framework_default": false + } + ], + "lifecycle_paths": [ + { + "id": "lifecycle_path_e1461ac0bc618003", + "artifact_ids": [ + "artifact_2d4eceb282cb58d6" + ], + "stages": [ + { + "stage": "revoke", + "evidence_ids": [ + "evidence_4e76a3048eeb65c6" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_981eddef51e17919", + "artifact_ids": [ + "artifact_4dc9e49ac137a92c", + "artifact_c807d79fde47d60c", + "artifact_e34a318309f55157" + ], + "stages": [ + { + "stage": "revoke", + "evidence_ids": [ + "evidence_08fb01f27b9526e6", + "evidence_46e713f862bd7a7a", + "evidence_9d5c6bbd4b320590" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_1503d38d0577da92", + "artifact_ids": [ + "artifact_99ec026c78bdccbb" + ], + "stages": [ + { + "stage": "store", + "evidence_ids": [ + "evidence_99ec026c78bdccbb", + "evidence_ca1fe3d007a72505", + "evidence_ec9bef1d37b762fd" + ] + }, + { + "stage": "transmit", + "evidence_ids": [ + "evidence_1c3527e01b7878d2", + "evidence_1ec1adbc2e3731f8", + "evidence_b696d1f40628568a", + "evidence_c6eb4d0f4c5683ab" + ] + }, + { + "stage": "expire", + "evidence_ids": [ + "evidence_054287c83ffbc9d9", + "evidence_a720310d31ee0f13" + ] + } + ], + "confidence": "low", + "dynamic": false, + "reviewer_question": "Which framework version and deployment settings determine lifecycle behavior for `sessionid`?" + } + ], + "findings": [] +} \ No newline at end of file diff --git a/tests/integration/snapshots/express-cookie-session-lifecycle.json b/tests/integration/snapshots/express-cookie-session-lifecycle.json new file mode 100644 index 0000000..1c27f4d --- /dev/null +++ b/tests/integration/snapshots/express-cookie-session-lifecycle.json @@ -0,0 +1,3901 @@ +{ + "schema_version": "0.5.0", + "summary": { + "files_discovered": 2, + "files_scanned": 2, + "files_skipped": 0, + "diagnostics": [], + "worker_panic_count": 0 + }, + "files": [ + { + "path": "app.ts", + "language": "type_script", + "artifacts": [ + { + "id": "artifact_6f837089594b74c4", + "artifact_type": "opaque_bearer_token", + "display_name": "access_token", + "locations": [ + { + "path": "app.ts", + "line": 6, + "column": 1 + }, + { + "path": "app.ts", + "line": 7, + "column": 9 + }, + { + "path": "app.ts", + "line": 8, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_5f28697326110c85" + ], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_ad3b0634ef3c54d4", + "evidence_db7c57d295560814" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "javascript" + ] + }, + { + "id": "artifact_64c56fd5a910792c", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "app.ts", + "line": 6, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_4e63c7300962690f" + ], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express", + "scope:111" + ] + }, + { + "id": "artifact_5a72289a03ddd485", + "artifact_type": "signed_cookie", + "display_name": "session", + "locations": [ + { + "path": "app.ts", + "line": 8, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_5a72289a03ddd485", + "evidence_b275602954778885", + "evidence_bf3ab86dfecb4f83" + ], + "transmit": [ + "evidence_05cc4e424d4d22c6", + "evidence_60893ace05d1a945", + "evidence_b798deb3d44bb045", + "evidence_bfaf3eddb9c369eb" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_9fc5df1d137d1f72", + "evidence_e040efa573297113" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ], + "cookie_attributes": { + "http_only": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_b275602954778885" + ], + "confidence": "high" + }, + "secure": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_b798deb3d44bb045" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "lax", + "evidence_ids": [ + "evidence_60893ace05d1a945" + ], + "confidence": "high" + }, + "max_age": { + "state": "dynamic", + "value": "15 * 60 * 1000", + "evidence_ids": [ + "evidence_9fc5df1d137d1f72" + ], + "confidence": "medium" + }, + "expires": { + "state": "missing", + "evidence_ids": [ + "evidence_e040efa573297113" + ], + "confidence": "high" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_05cc4e424d4d22c6" + ], + "confidence": "low" + }, + "domain": { + "state": "missing", + "evidence_ids": [ + "evidence_bfaf3eddb9c369eb" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_7a9827578e77376e", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "app.ts", + "line": 17, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_fa34ee456988eedf" + ], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express", + "scope:402" + ] + }, + { + "id": "artifact_8dcee4862e48c13e", + "artifact_type": "session_cookie", + "display_name": "legacy_session", + "locations": [ + { + "path": "app.ts", + "line": 18, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_8dcee4862e48c13e", + "evidence_9638cf3168c14e11", + "evidence_df5a3400b3bd5568" + ], + "transmit": [ + "evidence_49b87b12f7780901", + "evidence_71ff2402f81dfcc0", + "evidence_7fbcf7c04e205441", + "evidence_894b46e27a69f2f1" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_278dad9b2113bd18", + "evidence_8d28c83b347146c6" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ], + "cookie_attributes": { + "http_only": { + "state": "missing", + "value": "false", + "evidence_ids": [ + "evidence_9638cf3168c14e11" + ], + "confidence": "high" + }, + "secure": { + "state": "missing", + "evidence_ids": [ + "evidence_894b46e27a69f2f1" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "none", + "evidence_ids": [ + "evidence_7fbcf7c04e205441" + ], + "confidence": "high" + }, + "max_age": { + "state": "missing", + "evidence_ids": [ + "evidence_8d28c83b347146c6" + ], + "confidence": "high" + }, + "expires": { + "state": "missing", + "evidence_ids": [ + "evidence_278dad9b2113bd18" + ], + "confidence": "high" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_49b87b12f7780901" + ], + "confidence": "low" + }, + "domain": { + "state": "missing", + "evidence_ids": [ + "evidence_71ff2402f81dfcc0" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_154fd160cf3f690a", + "artifact_type": "refresh_jwt", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 24, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_305ee20048c6d267" + ], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "javascript" + ] + }, + { + "id": "artifact_ec4e55657a78fe6a", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 24, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [ + "evidence_ef953c43774a0588" + ], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ] + }, + { + "id": "artifact_f963d258ec5440c8", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 24, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_7fa7941e2d1c0707" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ] + }, + { + "id": "artifact_faaf8106c79488c1", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 24, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_ffa3238b17ab9f6e" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ] + }, + { + "id": "artifact_faaf8106c79488c1", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 24, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_2900c77baaeed029" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ] + }, + { + "id": "artifact_151942804270435d", + "artifact_type": "unknown_token", + "display_name": "unknown_token", + "locations": [ + { + "path": "app.ts", + "line": 24, + "column": 1 + }, + { + "path": "app.ts", + "line": 26, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_267820b395eba278" + ], + "revoke": [ + "evidence_19d5635581c34e60", + "evidence_93b05c309b27bbf4" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "javascript", + "nextjs" + ] + }, + { + "id": "artifact_63d7d91a0736c3b6", + "artifact_type": "refresh_jwt", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 26, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_69cff3144d75bdc3" + ], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "javascript" + ] + }, + { + "id": "artifact_3d68a57fcf1a38a2", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 26, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_5bff9dee81659348" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "refresh" + ] + }, + { + "id": "artifact_76910dbf5c5d9aa5", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 28, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_232e817eb8f7f9ee", + "evidence_752ffe3fd38ec118", + "evidence_76910dbf5c5d9aa5" + ], + "transmit": [ + "evidence_0375d1c6f42c91c0", + "evidence_3ce0cc0488566657", + "evidence_4206339cd0e7ae44", + "evidence_ee3153fb557b84d0" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_090de3fc0c51f0a8", + "evidence_9117595604464e2e" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ], + "cookie_attributes": { + "http_only": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_232e817eb8f7f9ee" + ], + "confidence": "high" + }, + "secure": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_4206339cd0e7ae44" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "strict", + "evidence_ids": [ + "evidence_0375d1c6f42c91c0" + ], + "confidence": "high" + }, + "max_age": { + "state": "missing", + "evidence_ids": [ + "evidence_9117595604464e2e" + ], + "confidence": "high" + }, + "expires": { + "state": "missing", + "evidence_ids": [ + "evidence_090de3fc0c51f0a8" + ], + "confidence": "high" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_3ce0cc0488566657" + ], + "confidence": "low" + }, + "domain": { + "state": "missing", + "evidence_ids": [ + "evidence_ee3153fb557b84d0" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_85de78ac6aa3bc33", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 28, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_6fd6657096c7cb64" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "refresh" + ] + }, + { + "id": "artifact_eac51c20eb23c659", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 28, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_2db359b7b6469528" + ], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "refresh" + ] + }, + { + "id": "artifact_f4f9e68f679d0cfc", + "artifact_type": "unknown", + "display_name": "logout", + "locations": [ + { + "path": "app.ts", + "line": 35, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_655039e7e2d8798c" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ] + }, + { + "id": "artifact_bdfbab6a12855f82", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 35, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_891184dd9d805d4d" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ] + }, + { + "id": "artifact_e0ff886aac6b2e9f", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "app.ts", + "line": 35, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_e598ec1bf7c310fd" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "javascript" + ] + }, + { + "id": "artifact_f40c4f1b8f755160", + "artifact_type": "opaque_bearer_token", + "display_name": "session_id", + "locations": [ + { + "path": "app.ts", + "line": 35, + "column": 1 + }, + { + "path": "app.ts", + "line": 36, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_0ee1c1d13af4e500", + "evidence_91a717f6ce1e2113" + ], + "expire": [], + "introspect": [ + "evidence_48674ab370561cc2", + "evidence_7e45838a032ab2e5", + "evidence_9c80d870069c511d", + "evidence_ded9725f66a2bc9a" + ] + }, + "confidence": "high", + "framework_hints": [ + "javascript" + ], + "token_boundary_attributes": { + "service": { + "state": "present", + "value": "server", + "evidence_ids": [ + "evidence_9c80d870069c511d", + "evidence_ded9725f66a2bc9a" + ], + "confidence": "high" + }, + "trust_boundary": { + "state": "present", + "value": "server", + "evidence_ids": [ + "evidence_48674ab370561cc2", + "evidence_7e45838a032ab2e5" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_ccce4e2d7769b026", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "app.ts", + "line": 36, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_a765a8bfc656851c" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "javascript" + ] + }, + { + "id": "artifact_f5fadee3e8437b01", + "artifact_type": "session_cookie", + "display_name": "session", + "locations": [ + { + "path": "app.ts", + "line": 37, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_bceedbcb218327ae" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ] + }, + { + "id": "artifact_9880b67e3360e95a", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 38, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_af325d42d3c49c7e" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ] + }, + { + "id": "artifact_93d2ba050ef7b6a8", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 42, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_abfcc0c427dc8c0b" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "javascript" + ] + } + ], + "evidence": [ + { + "id": "evidence_4e63c7300962690f", + "lifecycle_stage": "issue", + "location": { + "path": "app.ts", + "line": 6, + "column": 1 + }, + "detector_id": "session.auth_transition", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (_request, response) => {\n const accessToken: \"[REDACTED]\";\n response.cookie(\"[REDACTED]\", accessToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n maxAge: 15 * 60 * 1000,\n signed: true,\n });\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_db7c57d295560814", + "lifecycle_stage": "expire", + "location": { + "path": "app.ts", + "line": 6, + "column": 1 + }, + "detector_id": "bearer.expire", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (_request, response) => {\n const accessToken = \"[REDACTED]\";\n response.cookie(\"[REDACTED]\", accessToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n maxAge: 15 * 60 * 1000,\n signed: true,\n });\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_5f28697326110c85", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 7, + "column": 9 + }, + "detector_id": "bearer.literal.static", + "confidence": "high", + "excerpt": "accessToken = \"[REDACTED]\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_bf3ab86dfecb4f83", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 8, + "column": 3 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_5a72289a03ddd485", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 8, + "column": 3 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": " const [REDACTED] = \"[REDACTED]\";\n response.cookie(\"session\", [REDACTED], {\n httpOnly: true,", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_bfaf3eddb9c369eb", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 8, + "column": 3 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "high", + "excerpt": "Domain is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_05cc4e424d4d22c6", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 8, + "column": 3 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for express", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_ad3b0634ef3c54d4", + "lifecycle_stage": "expire", + "location": { + "path": "app.ts", + "line": 8, + "column": 3 + }, + "detector_id": "bearer.expire", + "confidence": "high", + "excerpt": "response.cookie(\"[REDACTED]\", accessToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n maxAge: 15 * 60 * 1000,\n signed: true,\n })", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_e040efa573297113", + "lifecycle_stage": "expire", + "location": { + "path": "app.ts", + "line": 8, + "column": 3 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "high", + "excerpt": "Expires is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_b275602954778885", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 9, + "column": 15 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "HttpOnly: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_b798deb3d44bb045", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 10, + "column": 13 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "Secure: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_60893ace05d1a945", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 11, + "column": 15 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SameSite: \"lax\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9fc5df1d137d1f72", + "lifecycle_stage": "expire", + "location": { + "path": "app.ts", + "line": 12, + "column": 13 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "medium", + "excerpt": "Max-Age: 15 * 60 * 1000", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_fa34ee456988eedf", + "lifecycle_stage": "issue", + "location": { + "path": "app.ts", + "line": 17, + "column": 1 + }, + "detector_id": "session.auth_transition", + "confidence": "high", + "excerpt": "app.post(\"/legacy-login\", (_request, response) => {\n response.cookie(\"legacy_session\", \"[REDACTED]\", {\n httpOnly: false,\n sameSite: \"none\",\n });\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_df5a3400b3bd5568", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 18, + "column": 3 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_8dcee4862e48c13e", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 18, + "column": 3 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": "app.post(\"/legacy-login\", (_request, response) => {\n response.cookie(\"legacy_session\", [REDACTED], {\n httpOnly: false,", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_71ff2402f81dfcc0", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 18, + "column": 3 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "high", + "excerpt": "Domain is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_49b87b12f7780901", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 18, + "column": 3 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for express", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_894b46e27a69f2f1", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 18, + "column": 3 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "Secure is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_278dad9b2113bd18", + "lifecycle_stage": "expire", + "location": { + "path": "app.ts", + "line": 18, + "column": 3 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "high", + "excerpt": "Expires is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_8d28c83b347146c6", + "lifecycle_stage": "expire", + "location": { + "path": "app.ts", + "line": 18, + "column": 3 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "high", + "excerpt": "Max-Age is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9638cf3168c14e11", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 19, + "column": 15 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "HttpOnly: false", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_7fbcf7c04e205441", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 20, + "column": 15 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SameSite: \"none\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_ef953c43774a0588", + "lifecycle_stage": "validate", + "location": { + "path": "app.ts", + "line": 24, + "column": 1 + }, + "detector_id": "refresh.validate", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n const previousRefreshToken = request.cookies?.refresh_token ?? \"[REDACTED]\";\n revokeRefreshToken(previousRefreshToken);\n const rotatedRefreshToken: \"[REDACTED]\";\n response.cookie(\"[REDACTED]\", rotatedRefreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n });\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_267820b395eba278", + "lifecycle_stage": "refresh", + "location": { + "path": "app.ts", + "line": 24, + "column": 1 + }, + "detector_id": "bearer.rotate", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n const previousRefreshToken = request.cookies?.refresh_token ?? \"[REDACTED]\";\n revokeRefreshToken(previousRefreshToken);\n const rotatedRefreshToken = \"[REDACTED]\";\n response.cookie(\"[REDACTED]\", rotatedRefreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n });\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_7fa7941e2d1c0707", + "lifecycle_stage": "refresh", + "location": { + "path": "app.ts", + "line": 24, + "column": 1 + }, + "detector_id": "refresh.handler", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n const previousRefreshToken = request.cookies?.refresh_token ?? \"[REDACTED]\";\n revokeRefreshToken(previousRefreshToken);\n const rotatedRefreshToken: \"[REDACTED]\";\n response.cookie(\"[REDACTED]\", rotatedRefreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n });\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_ffa3238b17ab9f6e", + "lifecycle_stage": "refresh", + "location": { + "path": "app.ts", + "line": 24, + "column": 1 + }, + "detector_id": "refresh.rotate", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n const previousRefreshToken = request.cookies?.refresh_token ?? \"[REDACTED]\";\n revokeRefreshToken(previousRefreshToken);\n const rotatedRefreshToken: \"[REDACTED]\";\n response.cookie(\"[REDACTED]\", rotatedRefreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n });\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_19d5635581c34e60", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 24, + "column": 1 + }, + "detector_id": "bearer.revoke", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n const previousRefreshToken = request.cookies?.refresh_token ?? \"[REDACTED]\";\n revokeRefreshToken(previousRefreshToken);\n const rotatedRefreshToken = \"[REDACTED]\";\n response.cookie(\"[REDACTED]\", rotatedRefreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n });\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_305ee20048c6d267", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 24, + "column": 1 + }, + "detector_id": "logout.token_revoke", + "confidence": "medium", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n const previousRefreshToken = request.cookies?.refresh_token ?? \"[REDACTED]\";\n revokeRefreshToken(previousRefreshToken);\n const rotatedRefreshToken: \"[REDACTED]\";\n response.cookie(\"[REDACTED]\", rotatedRefreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n });\n})", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_2900c77baaeed029", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 24, + "column": 1 + }, + "detector_id": "refresh.rotate", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n const previousRefreshToken = request.cookies?.refresh_token ?? \"[REDACTED]\";\n revokeRefreshToken(previousRefreshToken);\n const rotatedRefreshToken: \"[REDACTED]\";\n response.cookie(\"[REDACTED]\", rotatedRefreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n });\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_93b05c309b27bbf4", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 26, + "column": 3 + }, + "detector_id": "bearer.revoke", + "confidence": "high", + "excerpt": "revokeRefreshToken(previousRefreshToken)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_69cff3144d75bdc3", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 26, + "column": 3 + }, + "detector_id": "logout.token_revoke", + "confidence": "medium", + "excerpt": "revokeRefreshToken(previousRefreshToken)", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_5bff9dee81659348", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 26, + "column": 3 + }, + "detector_id": "refresh.revoke", + "confidence": "high", + "excerpt": "revokeRefreshToken(previousRefreshToken)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_752ffe3fd38ec118", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 28, + "column": 3 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_76910dbf5c5d9aa5", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 28, + "column": 3 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": " const [REDACTED] = \"[REDACTED]\";\n response.cookie(\"refresh_token\", [REDACTED], {\n httpOnly: true,", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2db359b7b6469528", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 28, + "column": 3 + }, + "detector_id": "refresh.store", + "confidence": "high", + "excerpt": "response.cookie(\"[REDACTED]\", rotatedRefreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n })", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_ee3153fb557b84d0", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 28, + "column": 3 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "high", + "excerpt": "Domain is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_3ce0cc0488566657", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 28, + "column": 3 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for express", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_6fd6657096c7cb64", + "lifecycle_stage": "refresh", + "location": { + "path": "app.ts", + "line": 28, + "column": 3 + }, + "detector_id": "refresh.rotate", + "confidence": "high", + "excerpt": "response.cookie(\"[REDACTED]\", rotatedRefreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n })", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_090de3fc0c51f0a8", + "lifecycle_stage": "expire", + "location": { + "path": "app.ts", + "line": 28, + "column": 3 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "high", + "excerpt": "Expires is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9117595604464e2e", + "lifecycle_stage": "expire", + "location": { + "path": "app.ts", + "line": 28, + "column": 3 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "high", + "excerpt": "Max-Age is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_232e817eb8f7f9ee", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 29, + "column": 15 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "HttpOnly: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_4206339cd0e7ae44", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 30, + "column": 13 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "Secure: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_0375d1c6f42c91c0", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 31, + "column": 15 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SameSite: \"strict\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_891184dd9d805d4d", + "lifecycle_stage": "refresh", + "location": { + "path": "app.ts", + "line": 35, + "column": 1 + }, + "detector_id": "refresh.handler", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n destroyServerSession(request.sessionID);\n response.clearCookie(\"[REDACTED]\");\n response.clearCookie(\"[REDACTED]\");\n response.sendStatus(204);\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_0ee1c1d13af4e500", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 35, + "column": 1 + }, + "detector_id": "bearer.revoke", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n destroyServerSession(request.sessionID);\n response.clearCookie(\"[REDACTED]\");\n response.clearCookie(\"[REDACTED]\");\n response.sendStatus(204);\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_655039e7e2d8798c", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 35, + "column": 1 + }, + "detector_id": "logout.handler", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n destroyServerSession(request.sessionID);\n response.clearCookie(\"[REDACTED]\");\n response.clearCookie(\"[REDACTED]\");\n response.sendStatus(204);\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_e598ec1bf7c310fd", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 35, + "column": 1 + }, + "detector_id": "logout.session_destroy", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n destroyServerSession(request.sessionID);\n response.clearCookie(\"[REDACTED]\");\n response.clearCookie(\"[REDACTED]\");\n response.sendStatus(204);\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_ded9725f66a2bc9a", + "lifecycle_stage": "introspect", + "location": { + "path": "app.ts", + "line": 35, + "column": 1 + }, + "detector_id": "bearer.boundary.service", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n destroyServerSession(request.sessionID);\n response.clearCookie(\"[REDACTED]\");\n response.clearCookie(\"[REDACTED]\");\n response.sendStatus(204);\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_48674ab370561cc2", + "lifecycle_stage": "introspect", + "location": { + "path": "app.ts", + "line": 35, + "column": 1 + }, + "detector_id": "bearer.boundary.trust_boundary", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n destroyServerSession(request.sessionID);\n response.clearCookie(\"[REDACTED]\");\n response.clearCookie(\"[REDACTED]\");\n response.sendStatus(204);\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_91a717f6ce1e2113", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 36, + "column": 3 + }, + "detector_id": "bearer.revoke", + "confidence": "high", + "excerpt": "destroyServerSession(request.sessionID)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_a765a8bfc656851c", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 36, + "column": 3 + }, + "detector_id": "logout.session_destroy", + "confidence": "high", + "excerpt": "destroyServerSession(request.sessionID)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9c80d870069c511d", + "lifecycle_stage": "introspect", + "location": { + "path": "app.ts", + "line": 36, + "column": 3 + }, + "detector_id": "bearer.boundary.service", + "confidence": "high", + "excerpt": "destroyServerSession(request.sessionID)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_7e45838a032ab2e5", + "lifecycle_stage": "introspect", + "location": { + "path": "app.ts", + "line": 36, + "column": 3 + }, + "detector_id": "bearer.boundary.trust_boundary", + "confidence": "high", + "excerpt": "destroyServerSession(request.sessionID)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_bceedbcb218327ae", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 37, + "column": 3 + }, + "detector_id": "logout.cookie_clear", + "confidence": "high", + "excerpt": "response.clearCookie(\"session\")", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_af325d42d3c49c7e", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 38, + "column": 3 + }, + "detector_id": "logout.cookie_clear", + "confidence": "high", + "excerpt": "response.clearCookie(\"[REDACTED]\")", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_abfcc0c427dc8c0b", + "lifecycle_stage": "refresh", + "location": { + "path": "app.ts", + "line": 42, + "column": 1 + }, + "detector_id": "refresh.handler", + "confidence": "medium", + "excerpt": "function revokeRefreshToken(_token: string) {\n return signingSecret.length > 0;\n}", + "dynamic": true, + "framework_default": false + } + ], + "diagnostics": [], + "skipped_reason": null + }, + { + "path": "expected.json", + "language": "json", + "artifacts": [], + "evidence": [], + "diagnostics": [], + "skipped_reason": null + } + ], + "artifacts": [ + { + "id": "artifact_6f837089594b74c4", + "artifact_type": "opaque_bearer_token", + "display_name": "access_token", + "locations": [ + { + "path": "app.ts", + "line": 6, + "column": 1 + }, + { + "path": "app.ts", + "line": 7, + "column": 9 + }, + { + "path": "app.ts", + "line": 8, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_5f28697326110c85" + ], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_ad3b0634ef3c54d4", + "evidence_db7c57d295560814" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "javascript" + ] + }, + { + "id": "artifact_64c56fd5a910792c", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "app.ts", + "line": 6, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_4e63c7300962690f" + ], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express", + "scope:111" + ] + }, + { + "id": "artifact_5a72289a03ddd485", + "artifact_type": "signed_cookie", + "display_name": "session", + "locations": [ + { + "path": "app.ts", + "line": 8, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_5a72289a03ddd485", + "evidence_b275602954778885", + "evidence_bf3ab86dfecb4f83" + ], + "transmit": [ + "evidence_05cc4e424d4d22c6", + "evidence_60893ace05d1a945", + "evidence_b798deb3d44bb045", + "evidence_bfaf3eddb9c369eb" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_9fc5df1d137d1f72", + "evidence_e040efa573297113" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ], + "cookie_attributes": { + "http_only": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_b275602954778885" + ], + "confidence": "high" + }, + "secure": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_b798deb3d44bb045" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "lax", + "evidence_ids": [ + "evidence_60893ace05d1a945" + ], + "confidence": "high" + }, + "max_age": { + "state": "dynamic", + "value": "15 * 60 * 1000", + "evidence_ids": [ + "evidence_9fc5df1d137d1f72" + ], + "confidence": "medium" + }, + "expires": { + "state": "missing", + "evidence_ids": [ + "evidence_e040efa573297113" + ], + "confidence": "high" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_05cc4e424d4d22c6" + ], + "confidence": "low" + }, + "domain": { + "state": "missing", + "evidence_ids": [ + "evidence_bfaf3eddb9c369eb" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_7a9827578e77376e", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "app.ts", + "line": 17, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_fa34ee456988eedf" + ], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express", + "scope:402" + ] + }, + { + "id": "artifact_8dcee4862e48c13e", + "artifact_type": "session_cookie", + "display_name": "legacy_session", + "locations": [ + { + "path": "app.ts", + "line": 18, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_8dcee4862e48c13e", + "evidence_9638cf3168c14e11", + "evidence_df5a3400b3bd5568" + ], + "transmit": [ + "evidence_49b87b12f7780901", + "evidence_71ff2402f81dfcc0", + "evidence_7fbcf7c04e205441", + "evidence_894b46e27a69f2f1" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_278dad9b2113bd18", + "evidence_8d28c83b347146c6" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ], + "cookie_attributes": { + "http_only": { + "state": "missing", + "value": "false", + "evidence_ids": [ + "evidence_9638cf3168c14e11" + ], + "confidence": "high" + }, + "secure": { + "state": "missing", + "evidence_ids": [ + "evidence_894b46e27a69f2f1" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "none", + "evidence_ids": [ + "evidence_7fbcf7c04e205441" + ], + "confidence": "high" + }, + "max_age": { + "state": "missing", + "evidence_ids": [ + "evidence_8d28c83b347146c6" + ], + "confidence": "high" + }, + "expires": { + "state": "missing", + "evidence_ids": [ + "evidence_278dad9b2113bd18" + ], + "confidence": "high" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_49b87b12f7780901" + ], + "confidence": "low" + }, + "domain": { + "state": "missing", + "evidence_ids": [ + "evidence_71ff2402f81dfcc0" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_154fd160cf3f690a", + "artifact_type": "refresh_jwt", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 24, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_305ee20048c6d267" + ], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "javascript" + ] + }, + { + "id": "artifact_ec4e55657a78fe6a", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 24, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [ + "evidence_ef953c43774a0588" + ], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ] + }, + { + "id": "artifact_f963d258ec5440c8", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 24, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_7fa7941e2d1c0707" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ] + }, + { + "id": "artifact_faaf8106c79488c1", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 24, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_ffa3238b17ab9f6e" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ] + }, + { + "id": "artifact_faaf8106c79488c1", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 24, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_2900c77baaeed029" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ] + }, + { + "id": "artifact_151942804270435d", + "artifact_type": "unknown_token", + "display_name": "unknown_token", + "locations": [ + { + "path": "app.ts", + "line": 24, + "column": 1 + }, + { + "path": "app.ts", + "line": 26, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_267820b395eba278" + ], + "revoke": [ + "evidence_19d5635581c34e60", + "evidence_93b05c309b27bbf4" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "javascript", + "nextjs" + ] + }, + { + "id": "artifact_63d7d91a0736c3b6", + "artifact_type": "refresh_jwt", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 26, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_69cff3144d75bdc3" + ], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "javascript" + ] + }, + { + "id": "artifact_3d68a57fcf1a38a2", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 26, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_5bff9dee81659348" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "refresh" + ] + }, + { + "id": "artifact_76910dbf5c5d9aa5", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 28, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_232e817eb8f7f9ee", + "evidence_752ffe3fd38ec118", + "evidence_76910dbf5c5d9aa5" + ], + "transmit": [ + "evidence_0375d1c6f42c91c0", + "evidence_3ce0cc0488566657", + "evidence_4206339cd0e7ae44", + "evidence_ee3153fb557b84d0" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_090de3fc0c51f0a8", + "evidence_9117595604464e2e" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ], + "cookie_attributes": { + "http_only": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_232e817eb8f7f9ee" + ], + "confidence": "high" + }, + "secure": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_4206339cd0e7ae44" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "strict", + "evidence_ids": [ + "evidence_0375d1c6f42c91c0" + ], + "confidence": "high" + }, + "max_age": { + "state": "missing", + "evidence_ids": [ + "evidence_9117595604464e2e" + ], + "confidence": "high" + }, + "expires": { + "state": "missing", + "evidence_ids": [ + "evidence_090de3fc0c51f0a8" + ], + "confidence": "high" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_3ce0cc0488566657" + ], + "confidence": "low" + }, + "domain": { + "state": "missing", + "evidence_ids": [ + "evidence_ee3153fb557b84d0" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_85de78ac6aa3bc33", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 28, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_6fd6657096c7cb64" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "refresh" + ] + }, + { + "id": "artifact_eac51c20eb23c659", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 28, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_2db359b7b6469528" + ], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "refresh" + ] + }, + { + "id": "artifact_f4f9e68f679d0cfc", + "artifact_type": "unknown", + "display_name": "logout", + "locations": [ + { + "path": "app.ts", + "line": 35, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_655039e7e2d8798c" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ] + }, + { + "id": "artifact_bdfbab6a12855f82", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 35, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_891184dd9d805d4d" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ] + }, + { + "id": "artifact_e0ff886aac6b2e9f", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "app.ts", + "line": 35, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_e598ec1bf7c310fd" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "javascript" + ] + }, + { + "id": "artifact_f40c4f1b8f755160", + "artifact_type": "opaque_bearer_token", + "display_name": "session_id", + "locations": [ + { + "path": "app.ts", + "line": 35, + "column": 1 + }, + { + "path": "app.ts", + "line": 36, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_0ee1c1d13af4e500", + "evidence_91a717f6ce1e2113" + ], + "expire": [], + "introspect": [ + "evidence_48674ab370561cc2", + "evidence_7e45838a032ab2e5", + "evidence_9c80d870069c511d", + "evidence_ded9725f66a2bc9a" + ] + }, + "confidence": "high", + "framework_hints": [ + "javascript" + ], + "token_boundary_attributes": { + "service": { + "state": "present", + "value": "server", + "evidence_ids": [ + "evidence_9c80d870069c511d", + "evidence_ded9725f66a2bc9a" + ], + "confidence": "high" + }, + "trust_boundary": { + "state": "present", + "value": "server", + "evidence_ids": [ + "evidence_48674ab370561cc2", + "evidence_7e45838a032ab2e5" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_ccce4e2d7769b026", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "app.ts", + "line": 36, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_a765a8bfc656851c" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "javascript" + ] + }, + { + "id": "artifact_f5fadee3e8437b01", + "artifact_type": "session_cookie", + "display_name": "session", + "locations": [ + { + "path": "app.ts", + "line": 37, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_bceedbcb218327ae" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ] + }, + { + "id": "artifact_9880b67e3360e95a", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 38, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_af325d42d3c49c7e" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "express" + ] + }, + { + "id": "artifact_93d2ba050ef7b6a8", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "app.ts", + "line": 42, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_abfcc0c427dc8c0b" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "javascript" + ] + } + ], + "evidence": [ + { + "id": "evidence_4e63c7300962690f", + "lifecycle_stage": "issue", + "location": { + "path": "app.ts", + "line": 6, + "column": 1 + }, + "detector_id": "session.auth_transition", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (_request, response) => {\n const accessToken: \"[REDACTED]\";\n response.cookie(\"[REDACTED]\", accessToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n maxAge: 15 * 60 * 1000,\n signed: true,\n });\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_db7c57d295560814", + "lifecycle_stage": "expire", + "location": { + "path": "app.ts", + "line": 6, + "column": 1 + }, + "detector_id": "bearer.expire", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (_request, response) => {\n const accessToken = \"[REDACTED]\";\n response.cookie(\"[REDACTED]\", accessToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n maxAge: 15 * 60 * 1000,\n signed: true,\n });\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_5f28697326110c85", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 7, + "column": 9 + }, + "detector_id": "bearer.literal.static", + "confidence": "high", + "excerpt": "accessToken = \"[REDACTED]\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_bf3ab86dfecb4f83", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 8, + "column": 3 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_5a72289a03ddd485", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 8, + "column": 3 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": " const [REDACTED] = \"[REDACTED]\";\n response.cookie(\"session\", [REDACTED], {\n httpOnly: true,", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_bfaf3eddb9c369eb", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 8, + "column": 3 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "high", + "excerpt": "Domain is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_05cc4e424d4d22c6", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 8, + "column": 3 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for express", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_ad3b0634ef3c54d4", + "lifecycle_stage": "expire", + "location": { + "path": "app.ts", + "line": 8, + "column": 3 + }, + "detector_id": "bearer.expire", + "confidence": "high", + "excerpt": "response.cookie(\"[REDACTED]\", accessToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n maxAge: 15 * 60 * 1000,\n signed: true,\n })", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_e040efa573297113", + "lifecycle_stage": "expire", + "location": { + "path": "app.ts", + "line": 8, + "column": 3 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "high", + "excerpt": "Expires is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_b275602954778885", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 9, + "column": 15 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "HttpOnly: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_b798deb3d44bb045", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 10, + "column": 13 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "Secure: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_60893ace05d1a945", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 11, + "column": 15 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SameSite: \"lax\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9fc5df1d137d1f72", + "lifecycle_stage": "expire", + "location": { + "path": "app.ts", + "line": 12, + "column": 13 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "medium", + "excerpt": "Max-Age: 15 * 60 * 1000", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_fa34ee456988eedf", + "lifecycle_stage": "issue", + "location": { + "path": "app.ts", + "line": 17, + "column": 1 + }, + "detector_id": "session.auth_transition", + "confidence": "high", + "excerpt": "app.post(\"/legacy-login\", (_request, response) => {\n response.cookie(\"legacy_session\", \"[REDACTED]\", {\n httpOnly: false,\n sameSite: \"none\",\n });\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_df5a3400b3bd5568", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 18, + "column": 3 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_8dcee4862e48c13e", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 18, + "column": 3 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": "app.post(\"/legacy-login\", (_request, response) => {\n response.cookie(\"legacy_session\", [REDACTED], {\n httpOnly: false,", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_71ff2402f81dfcc0", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 18, + "column": 3 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "high", + "excerpt": "Domain is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_49b87b12f7780901", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 18, + "column": 3 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for express", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_894b46e27a69f2f1", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 18, + "column": 3 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "Secure is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_278dad9b2113bd18", + "lifecycle_stage": "expire", + "location": { + "path": "app.ts", + "line": 18, + "column": 3 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "high", + "excerpt": "Expires is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_8d28c83b347146c6", + "lifecycle_stage": "expire", + "location": { + "path": "app.ts", + "line": 18, + "column": 3 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "high", + "excerpt": "Max-Age is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9638cf3168c14e11", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 19, + "column": 15 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "HttpOnly: false", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_7fbcf7c04e205441", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 20, + "column": 15 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SameSite: \"none\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_ef953c43774a0588", + "lifecycle_stage": "validate", + "location": { + "path": "app.ts", + "line": 24, + "column": 1 + }, + "detector_id": "refresh.validate", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n const previousRefreshToken = request.cookies?.refresh_token ?? \"[REDACTED]\";\n revokeRefreshToken(previousRefreshToken);\n const rotatedRefreshToken: \"[REDACTED]\";\n response.cookie(\"[REDACTED]\", rotatedRefreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n });\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_267820b395eba278", + "lifecycle_stage": "refresh", + "location": { + "path": "app.ts", + "line": 24, + "column": 1 + }, + "detector_id": "bearer.rotate", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n const previousRefreshToken = request.cookies?.refresh_token ?? \"[REDACTED]\";\n revokeRefreshToken(previousRefreshToken);\n const rotatedRefreshToken = \"[REDACTED]\";\n response.cookie(\"[REDACTED]\", rotatedRefreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n });\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_7fa7941e2d1c0707", + "lifecycle_stage": "refresh", + "location": { + "path": "app.ts", + "line": 24, + "column": 1 + }, + "detector_id": "refresh.handler", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n const previousRefreshToken = request.cookies?.refresh_token ?? \"[REDACTED]\";\n revokeRefreshToken(previousRefreshToken);\n const rotatedRefreshToken: \"[REDACTED]\";\n response.cookie(\"[REDACTED]\", rotatedRefreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n });\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_ffa3238b17ab9f6e", + "lifecycle_stage": "refresh", + "location": { + "path": "app.ts", + "line": 24, + "column": 1 + }, + "detector_id": "refresh.rotate", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n const previousRefreshToken = request.cookies?.refresh_token ?? \"[REDACTED]\";\n revokeRefreshToken(previousRefreshToken);\n const rotatedRefreshToken: \"[REDACTED]\";\n response.cookie(\"[REDACTED]\", rotatedRefreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n });\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_19d5635581c34e60", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 24, + "column": 1 + }, + "detector_id": "bearer.revoke", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n const previousRefreshToken = request.cookies?.refresh_token ?? \"[REDACTED]\";\n revokeRefreshToken(previousRefreshToken);\n const rotatedRefreshToken = \"[REDACTED]\";\n response.cookie(\"[REDACTED]\", rotatedRefreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n });\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_305ee20048c6d267", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 24, + "column": 1 + }, + "detector_id": "logout.token_revoke", + "confidence": "medium", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n const previousRefreshToken = request.cookies?.refresh_token ?? \"[REDACTED]\";\n revokeRefreshToken(previousRefreshToken);\n const rotatedRefreshToken: \"[REDACTED]\";\n response.cookie(\"[REDACTED]\", rotatedRefreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n });\n})", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_2900c77baaeed029", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 24, + "column": 1 + }, + "detector_id": "refresh.rotate", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n const previousRefreshToken = request.cookies?.refresh_token ?? \"[REDACTED]\";\n revokeRefreshToken(previousRefreshToken);\n const rotatedRefreshToken: \"[REDACTED]\";\n response.cookie(\"[REDACTED]\", rotatedRefreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n });\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_93b05c309b27bbf4", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 26, + "column": 3 + }, + "detector_id": "bearer.revoke", + "confidence": "high", + "excerpt": "revokeRefreshToken(previousRefreshToken)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_69cff3144d75bdc3", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 26, + "column": 3 + }, + "detector_id": "logout.token_revoke", + "confidence": "medium", + "excerpt": "revokeRefreshToken(previousRefreshToken)", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_5bff9dee81659348", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 26, + "column": 3 + }, + "detector_id": "refresh.revoke", + "confidence": "high", + "excerpt": "revokeRefreshToken(previousRefreshToken)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_752ffe3fd38ec118", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 28, + "column": 3 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_76910dbf5c5d9aa5", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 28, + "column": 3 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": " const [REDACTED] = \"[REDACTED]\";\n response.cookie(\"refresh_token\", [REDACTED], {\n httpOnly: true,", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2db359b7b6469528", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 28, + "column": 3 + }, + "detector_id": "refresh.store", + "confidence": "high", + "excerpt": "response.cookie(\"[REDACTED]\", rotatedRefreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n })", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_ee3153fb557b84d0", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 28, + "column": 3 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "high", + "excerpt": "Domain is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_3ce0cc0488566657", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 28, + "column": 3 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for express", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_6fd6657096c7cb64", + "lifecycle_stage": "refresh", + "location": { + "path": "app.ts", + "line": 28, + "column": 3 + }, + "detector_id": "refresh.rotate", + "confidence": "high", + "excerpt": "response.cookie(\"[REDACTED]\", rotatedRefreshToken, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n })", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_090de3fc0c51f0a8", + "lifecycle_stage": "expire", + "location": { + "path": "app.ts", + "line": 28, + "column": 3 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "high", + "excerpt": "Expires is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9117595604464e2e", + "lifecycle_stage": "expire", + "location": { + "path": "app.ts", + "line": 28, + "column": 3 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "high", + "excerpt": "Max-Age is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_232e817eb8f7f9ee", + "lifecycle_stage": "store", + "location": { + "path": "app.ts", + "line": 29, + "column": 15 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "HttpOnly: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_4206339cd0e7ae44", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 30, + "column": 13 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "Secure: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_0375d1c6f42c91c0", + "lifecycle_stage": "transmit", + "location": { + "path": "app.ts", + "line": 31, + "column": 15 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SameSite: \"strict\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_891184dd9d805d4d", + "lifecycle_stage": "refresh", + "location": { + "path": "app.ts", + "line": 35, + "column": 1 + }, + "detector_id": "refresh.handler", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n destroyServerSession(request.sessionID);\n response.clearCookie(\"[REDACTED]\");\n response.clearCookie(\"[REDACTED]\");\n response.sendStatus(204);\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_0ee1c1d13af4e500", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 35, + "column": 1 + }, + "detector_id": "bearer.revoke", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n destroyServerSession(request.sessionID);\n response.clearCookie(\"[REDACTED]\");\n response.clearCookie(\"[REDACTED]\");\n response.sendStatus(204);\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_655039e7e2d8798c", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 35, + "column": 1 + }, + "detector_id": "logout.handler", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n destroyServerSession(request.sessionID);\n response.clearCookie(\"[REDACTED]\");\n response.clearCookie(\"[REDACTED]\");\n response.sendStatus(204);\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_e598ec1bf7c310fd", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 35, + "column": 1 + }, + "detector_id": "logout.session_destroy", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n destroyServerSession(request.sessionID);\n response.clearCookie(\"[REDACTED]\");\n response.clearCookie(\"[REDACTED]\");\n response.sendStatus(204);\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_ded9725f66a2bc9a", + "lifecycle_stage": "introspect", + "location": { + "path": "app.ts", + "line": 35, + "column": 1 + }, + "detector_id": "bearer.boundary.service", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n destroyServerSession(request.sessionID);\n response.clearCookie(\"[REDACTED]\");\n response.clearCookie(\"[REDACTED]\");\n response.sendStatus(204);\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_48674ab370561cc2", + "lifecycle_stage": "introspect", + "location": { + "path": "app.ts", + "line": 35, + "column": 1 + }, + "detector_id": "bearer.boundary.trust_boundary", + "confidence": "high", + "excerpt": "app.post(\"[REDACTED]\", (request, response) => {\n destroyServerSession(request.sessionID);\n response.clearCookie(\"[REDACTED]\");\n response.clearCookie(\"[REDACTED]\");\n response.sendStatus(204);\n})", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_91a717f6ce1e2113", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 36, + "column": 3 + }, + "detector_id": "bearer.revoke", + "confidence": "high", + "excerpt": "destroyServerSession(request.sessionID)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_a765a8bfc656851c", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 36, + "column": 3 + }, + "detector_id": "logout.session_destroy", + "confidence": "high", + "excerpt": "destroyServerSession(request.sessionID)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9c80d870069c511d", + "lifecycle_stage": "introspect", + "location": { + "path": "app.ts", + "line": 36, + "column": 3 + }, + "detector_id": "bearer.boundary.service", + "confidence": "high", + "excerpt": "destroyServerSession(request.sessionID)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_7e45838a032ab2e5", + "lifecycle_stage": "introspect", + "location": { + "path": "app.ts", + "line": 36, + "column": 3 + }, + "detector_id": "bearer.boundary.trust_boundary", + "confidence": "high", + "excerpt": "destroyServerSession(request.sessionID)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_bceedbcb218327ae", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 37, + "column": 3 + }, + "detector_id": "logout.cookie_clear", + "confidence": "high", + "excerpt": "response.clearCookie(\"session\")", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_af325d42d3c49c7e", + "lifecycle_stage": "revoke", + "location": { + "path": "app.ts", + "line": 38, + "column": 3 + }, + "detector_id": "logout.cookie_clear", + "confidence": "high", + "excerpt": "response.clearCookie(\"[REDACTED]\")", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_abfcc0c427dc8c0b", + "lifecycle_stage": "refresh", + "location": { + "path": "app.ts", + "line": 42, + "column": 1 + }, + "detector_id": "refresh.handler", + "confidence": "medium", + "excerpt": "function revokeRefreshToken(_token: string) {\n return signingSecret.length > 0;\n}", + "dynamic": true, + "framework_default": false + } + ], + "lifecycle_paths": [ + { + "id": "lifecycle_path_4033ab72bb1235bf", + "artifact_ids": [ + "artifact_151942804270435d" + ], + "stages": [ + { + "stage": "refresh", + "evidence_ids": [ + "evidence_267820b395eba278" + ] + }, + { + "stage": "revoke", + "evidence_ids": [ + "evidence_19d5635581c34e60", + "evidence_93b05c309b27bbf4" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_5b475db68188077b", + "artifact_ids": [ + "artifact_154fd160cf3f690a", + "artifact_3d68a57fcf1a38a2", + "artifact_63d7d91a0736c3b6", + "artifact_76910dbf5c5d9aa5", + "artifact_85de78ac6aa3bc33", + "artifact_93d2ba050ef7b6a8", + "artifact_9880b67e3360e95a", + "artifact_bdfbab6a12855f82", + "artifact_eac51c20eb23c659", + "artifact_ec4e55657a78fe6a", + "artifact_f963d258ec5440c8", + "artifact_faaf8106c79488c1" + ], + "stages": [ + { + "stage": "store", + "evidence_ids": [ + "evidence_232e817eb8f7f9ee", + "evidence_2db359b7b6469528", + "evidence_752ffe3fd38ec118", + "evidence_76910dbf5c5d9aa5" + ] + }, + { + "stage": "transmit", + "evidence_ids": [ + "evidence_0375d1c6f42c91c0", + "evidence_3ce0cc0488566657", + "evidence_4206339cd0e7ae44", + "evidence_ee3153fb557b84d0" + ] + }, + { + "stage": "validate", + "evidence_ids": [ + "evidence_ef953c43774a0588" + ] + }, + { + "stage": "refresh", + "evidence_ids": [ + "evidence_6fd6657096c7cb64", + "evidence_7fa7941e2d1c0707", + "evidence_891184dd9d805d4d", + "evidence_abfcc0c427dc8c0b", + "evidence_ffa3238b17ab9f6e" + ] + }, + { + "stage": "revoke", + "evidence_ids": [ + "evidence_2900c77baaeed029", + "evidence_305ee20048c6d267", + "evidence_5bff9dee81659348", + "evidence_69cff3144d75bdc3", + "evidence_af325d42d3c49c7e" + ] + }, + { + "stage": "expire", + "evidence_ids": [ + "evidence_090de3fc0c51f0a8", + "evidence_9117595604464e2e" + ] + } + ], + "confidence": "low", + "dynamic": true, + "reviewer_question": "Which production code path determines the effective lifecycle behavior for `refresh_token`?" + }, + { + "id": "lifecycle_path_86c116b37f0e2ad3", + "artifact_ids": [ + "artifact_5a72289a03ddd485", + "artifact_ccce4e2d7769b026", + "artifact_e0ff886aac6b2e9f", + "artifact_f5fadee3e8437b01" + ], + "stages": [ + { + "stage": "store", + "evidence_ids": [ + "evidence_5a72289a03ddd485", + "evidence_b275602954778885", + "evidence_bf3ab86dfecb4f83" + ] + }, + { + "stage": "transmit", + "evidence_ids": [ + "evidence_05cc4e424d4d22c6", + "evidence_60893ace05d1a945", + "evidence_b798deb3d44bb045", + "evidence_bfaf3eddb9c369eb" + ] + }, + { + "stage": "revoke", + "evidence_ids": [ + "evidence_a765a8bfc656851c", + "evidence_bceedbcb218327ae", + "evidence_e598ec1bf7c310fd" + ] + }, + { + "stage": "expire", + "evidence_ids": [ + "evidence_9fc5df1d137d1f72", + "evidence_e040efa573297113" + ] + } + ], + "confidence": "low", + "dynamic": true, + "reviewer_question": "Which production code path determines the effective lifecycle behavior for `session`?" + }, + { + "id": "lifecycle_path_4c077575b3f09cbd", + "artifact_ids": [ + "artifact_64c56fd5a910792c" + ], + "stages": [ + { + "stage": "issue", + "evidence_ids": [ + "evidence_4e63c7300962690f" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_2e8eb975b1c526e6", + "artifact_ids": [ + "artifact_6f837089594b74c4" + ], + "stages": [ + { + "stage": "store", + "evidence_ids": [ + "evidence_5f28697326110c85" + ] + }, + { + "stage": "expire", + "evidence_ids": [ + "evidence_ad3b0634ef3c54d4", + "evidence_db7c57d295560814" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_572969cbd1e8798b", + "artifact_ids": [ + "artifact_7a9827578e77376e" + ], + "stages": [ + { + "stage": "issue", + "evidence_ids": [ + "evidence_fa34ee456988eedf" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_f569078327ea3a2d", + "artifact_ids": [ + "artifact_8dcee4862e48c13e" + ], + "stages": [ + { + "stage": "store", + "evidence_ids": [ + "evidence_8dcee4862e48c13e", + "evidence_9638cf3168c14e11", + "evidence_df5a3400b3bd5568" + ] + }, + { + "stage": "transmit", + "evidence_ids": [ + "evidence_49b87b12f7780901", + "evidence_71ff2402f81dfcc0", + "evidence_7fbcf7c04e205441", + "evidence_894b46e27a69f2f1" + ] + }, + { + "stage": "expire", + "evidence_ids": [ + "evidence_278dad9b2113bd18", + "evidence_8d28c83b347146c6" + ] + } + ], + "confidence": "low", + "dynamic": false, + "reviewer_question": "Which framework version and deployment settings determine lifecycle behavior for `legacy_session`?" + }, + { + "id": "lifecycle_path_0e964f654c165e5c", + "artifact_ids": [ + "artifact_f40c4f1b8f755160" + ], + "stages": [ + { + "stage": "revoke", + "evidence_ids": [ + "evidence_0ee1c1d13af4e500", + "evidence_91a717f6ce1e2113" + ] + }, + { + "stage": "introspect", + "evidence_ids": [ + "evidence_48674ab370561cc2", + "evidence_7e45838a032ab2e5", + "evidence_9c80d870069c511d", + "evidence_ded9725f66a2bc9a" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_805b1ae29e882bbe", + "artifact_ids": [ + "artifact_f4f9e68f679d0cfc" + ], + "stages": [ + { + "stage": "revoke", + "evidence_ids": [ + "evidence_655039e7e2d8798c" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + } + ], + "findings": [ + { + "id": "finding_9d395752f36e474c", + "category": "high_confidence_misconfiguration", + "severity": "high", + "artifact_ids": [ + "artifact_6f837089594b74c4" + ], + "evidence_ids": [ + "evidence_5f28697326110c85" + ], + "title": "Token `access_token` has static secret-like literal evidence", + "description": "Source evidence contains a static token/API-key style literal. The value is redacted in reports, but the code path should not carry runtime secrets in source.", + "suggested_fix": "Move runtime token values to approved secret storage and keep source/config references value-free.", + "reviewer_question": "Is this placeholder-only fixture code, or can production source contain a token value here?" + }, + { + "id": "finding_25b7beeb5b710c99", + "category": "high_confidence_misconfiguration", + "severity": "high", + "artifact_ids": [ + "artifact_8dcee4862e48c13e" + ], + "evidence_ids": [ + "evidence_7fbcf7c04e205441", + "evidence_894b46e27a69f2f1" + ], + "title": "Cookie `legacy_session` sets SameSite=None without Secure evidence", + "description": "SameSite=None was detected, but Secure attribute evidence was not detected for this cookie-setting call.", + "suggested_fix": "Set Secure whenever SameSite=None is used.", + "reviewer_question": "Is this cookie intentionally available in cross-site requests?" + }, + { + "id": "finding_55131de590e94521", + "category": "high_confidence_misconfiguration", + "severity": "high", + "artifact_ids": [ + "artifact_8dcee4862e48c13e" + ], + "evidence_ids": [ + "evidence_894b46e27a69f2f1" + ], + "title": "Cookie `legacy_session` does not set Secure", + "description": "No Secure attribute evidence was detected for this cookie-setting call.", + "suggested_fix": "Set Secure for cookies that should only be sent over HTTPS.", + "reviewer_question": "Is this cookie ever used in an externally reachable production environment?" + }, + { + "id": "finding_a38cf3af681ec5dc", + "category": "high_confidence_misconfiguration", + "severity": "high", + "artifact_ids": [ + "artifact_8dcee4862e48c13e" + ], + "evidence_ids": [ + "evidence_9638cf3168c14e11" + ], + "title": "Session-like cookie `legacy_session` does not set HttpOnly", + "description": "No HttpOnly attribute evidence was detected for this cookie-setting call.", + "suggested_fix": "Set HttpOnly on session cookies so client-side scripts cannot read them.", + "reviewer_question": "Is this cookie intended to be inaccessible to browser JavaScript?" + }, + { + "id": "finding_05e18a66378175d4", + "category": "lifecycle_gap", + "severity": "medium", + "artifact_ids": [ + "artifact_76910dbf5c5d9aa5" + ], + "evidence_ids": [ + "evidence_655039e7e2d8798c", + "evidence_6fd6657096c7cb64", + "evidence_7fa7941e2d1c0707", + "evidence_891184dd9d805d4d", + "evidence_abfcc0c427dc8c0b", + "evidence_ffa3238b17ab9f6e" + ], + "title": "Refresh token `refresh_token` has logout evidence without family revocation", + "description": "Logout and refresh-token lifecycle evidence were detected in linked source context, but no source-bound user-scoped or refresh-family revocation evidence was linked for the logout flow.", + "suggested_fix": "Revoke the user's refresh-token family, delete user-scoped refresh-token records, or remove the refresh-family cache key during logout.", + "reviewer_question": "Where does logout revoke every refresh token in the `refresh_token` family or for the current user?" + }, + { + "id": "finding_b39b8f710f933855", + "category": "dynamic_review_required", + "severity": "medium", + "artifact_ids": [ + "artifact_64c56fd5a910792c" + ], + "evidence_ids": [ + "evidence_4e63c7300962690f" + ], + "title": "Session regeneration evidence was not found near login", + "description": "An authentication transition was detected, but nearby static evidence did not show an explicit session regeneration, cookie reissue, or recognized framework-default rotation point.", + "suggested_fix": "Regenerate the server-side session with [REDACTED](...) after authentication and privilege changes before storing the authenticated user state.", + "reviewer_question": "Where is the session identifier rotated after this authentication transition?" + }, + { + "id": "finding_d475e20fdbc03e20", + "category": "dynamic_review_required", + "severity": "medium", + "artifact_ids": [ + "artifact_7a9827578e77376e" + ], + "evidence_ids": [ + "evidence_fa34ee456988eedf" + ], + "title": "Session regeneration evidence was not found near login", + "description": "An authentication transition was detected, but nearby static evidence did not show an explicit session regeneration, cookie reissue, or recognized framework-default rotation point.", + "suggested_fix": "Regenerate the server-side session with [REDACTED](...) after authentication and privilege changes before storing the authenticated user state.", + "reviewer_question": "Where is the session identifier rotated after this authentication transition?" + }, + { + "id": "finding_620ea7721befb84c", + "category": "lifecycle_gap", + "severity": "low", + "artifact_ids": [ + "artifact_8dcee4862e48c13e" + ], + "evidence_ids": [ + "evidence_278dad9b2113bd18", + "evidence_8d28c83b347146c6" + ], + "title": "Cookie `legacy_session` has no explicit expiry evidence", + "description": "No Max-Age or Expires evidence was detected for this cookie-setting call.", + "suggested_fix": "Add an explicit Max-Age or Expires value when the cookie should have a bounded lifetime.", + "reviewer_question": "Should this cookie be session-scoped, or should it have an explicit lifetime?" + }, + { + "id": "finding_2229884393d65240", + "category": "dynamic_review_required", + "severity": "low", + "artifact_ids": [ + "artifact_5a72289a03ddd485" + ], + "evidence_ids": [ + "evidence_9fc5df1d137d1f72", + "evidence_e040efa573297113" + ], + "title": "Cookie `session` has dynamic expiry evidence", + "description": "Cookie lifetime appears to depend on dynamic Max-Age or Expires evidence.", + "suggested_fix": "Confirm the effective cookie lifetime and document the production value.", + "reviewer_question": "What effective lifetime does this cookie have in production?" + } + ] +} \ No newline at end of file diff --git a/tests/integration/snapshots/fastapi-dependency-auth-lifecycle.json b/tests/integration/snapshots/fastapi-dependency-auth-lifecycle.json new file mode 100644 index 0000000..bd7a791 --- /dev/null +++ b/tests/integration/snapshots/fastapi-dependency-auth-lifecycle.json @@ -0,0 +1,2357 @@ +{ + "schema_version": "0.5.0", + "summary": { + "files_discovered": 2, + "files_scanned": 2, + "files_skipped": 0, + "diagnostics": [], + "worker_panic_count": 0 + }, + "files": [ + { + "path": "app.py", + "language": "python", + "artifacts": [ + { + "id": "artifact_1f6194ee35663737", + "artifact_type": "access_jwt", + "display_name": "access_jwt", + "locations": [ + { + "path": "app.py", + "line": 14, + "column": 12 + }, + { + "path": "app.py", + "line": 29, + "column": 12 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_04e27dc796edd948", + "evidence_1d120b1c0c19e112", + "evidence_485ff7f12a7a83f8", + "evidence_67ba10e56375217d", + "evidence_98eb78b1628dff5a", + "evidence_b1e065af58c3925c" + ], + "store": [], + "transmit": [], + "validate": [ + "evidence_0430950bd7d6b709", + "evidence_1a96e0988bbbf340", + "evidence_400f7ea0d2ddb252", + "evidence_67f9caad64bf13a6", + "evidence_6b8e2bb85b4a5c59", + "evidence_751429b89c10824f", + "evidence_8251d59500cb750b", + "evidence_879be86c887ffc25", + "evidence_9ec068fd376b7d89", + "evidence_b61b525c331695f4" + ], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_dd7490cd811b1d0c" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "pyjwt" + ], + "jwt_attributes": { + "operation": { + "state": "present", + "value": "issue, validate", + "evidence_ids": [ + "evidence_67ba10e56375217d", + "evidence_b61b525c331695f4" + ], + "confidence": "high" + }, + "algorithm": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_04e27dc796edd948", + "evidence_9ec068fd376b7d89" + ], + "confidence": "high" + }, + "key_reference": { + "state": "present", + "value": "JWT_SECRET", + "evidence_ids": [ + "evidence_879be86c887ffc25", + "evidence_98eb78b1628dff5a" + ], + "confidence": "high" + }, + "issuer": { + "state": "present", + "value": "ISSUER", + "evidence_ids": [ + "evidence_0430950bd7d6b709", + "evidence_485ff7f12a7a83f8" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_751429b89c10824f", + "evidence_b1e065af58c3925c" + ], + "confidence": "high" + }, + "expiration": { + "state": "present", + "value": "expires_at", + "evidence_ids": [ + "evidence_dd7490cd811b1d0c" + ], + "confidence": "high" + }, + "signature_verification": { + "state": "present", + "value": "verified", + "evidence_ids": [ + "evidence_6b8e2bb85b4a5c59" + ], + "confidence": "high" + }, + "expiry_enforcement": { + "state": "framework_default", + "value": "PyJWT decode default", + "evidence_ids": [ + "evidence_400f7ea0d2ddb252" + ], + "confidence": "low" + }, + "identity_claims": { + "subject": { + "state": "present", + "value": "user_id", + "evidence_ids": [ + "evidence_1d120b1c0c19e112" + ], + "confidence": "high" + }, + "user_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "tenant_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "org_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "workspace_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "roles": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "scopes": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "groups": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "email": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "email_verified": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "auth_method": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "auth_class": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + } + } + }, + "token_boundary_attributes": { + "issuer": { + "state": "present", + "value": "ISSUER", + "evidence_ids": [ + "evidence_0430950bd7d6b709", + "evidence_485ff7f12a7a83f8" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_751429b89c10824f", + "evidence_b1e065af58c3925c" + ], + "confidence": "high" + }, + "trust_boundary": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_751429b89c10824f", + "evidence_b1e065af58c3925c" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_6fe7bd9aa9441210", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "app.py", + "line": 39, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_bab2541ae5986d41" + ], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "fastapi", + "scope:898" + ] + }, + { + "id": "artifact_6662c62fe10196d2", + "artifact_type": "opaque_bearer_token", + "display_name": "access_token", + "locations": [ + { + "path": "app.py", + "line": 40, + "column": 5 + }, + { + "path": "app.py", + "line": 40, + "column": 13 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_0277e4a57e2be5d0" + ], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [ + "evidence_8f65a9c03ac6856e", + "evidence_d40d72776f6ad953" + ] + }, + "confidence": "high", + "framework_hints": [ + "python" + ], + "token_boundary_attributes": { + "issuer": { + "state": "present", + "evidence_ids": [ + "evidence_8f65a9c03ac6856e", + "evidence_d40d72776f6ad953" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_12991725538e196c", + "artifact_type": "session_cookie", + "display_name": "session", + "locations": [ + { + "path": "app.py", + "line": 41, + "column": 5 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_12991725538e196c", + "evidence_2e869cc62fb447f5", + "evidence_ee55e24a0d65191a" + ], + "transmit": [ + "evidence_063c471d6ac166fc", + "evidence_427e3aca09f2a901", + "evidence_4b35d34e5e07ffc4", + "evidence_61c3d2780025a546" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_51e68c6ad8120fd6", + "evidence_a7c8930136f027d4" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "python" + ], + "cookie_attributes": { + "http_only": { + "state": "present", + "value": "True", + "evidence_ids": [ + "evidence_2e869cc62fb447f5" + ], + "confidence": "high" + }, + "secure": { + "state": "present", + "value": "True", + "evidence_ids": [ + "evidence_61c3d2780025a546" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "lax", + "evidence_ids": [ + "evidence_4b35d34e5e07ffc4" + ], + "confidence": "high" + }, + "max_age": { + "state": "present", + "value": "900", + "evidence_ids": [ + "evidence_51e68c6ad8120fd6" + ], + "confidence": "high" + }, + "expires": { + "state": "framework_default", + "value": "none", + "evidence_ids": [ + "evidence_a7c8930136f027d4" + ], + "confidence": "low" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_427e3aca09f2a901" + ], + "confidence": "low" + }, + "domain": { + "state": "framework_default", + "value": "none", + "evidence_ids": [ + "evidence_063c471d6ac166fc" + ], + "confidence": "low" + } + } + }, + { + "id": "artifact_35be80ef8141f7cb", + "artifact_type": "unknown_token", + "display_name": "unknown_token", + "locations": [ + { + "path": "app.py", + "line": 55, + "column": 5 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_7e0f8ab5f17e8838" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "python" + ] + }, + { + "id": "artifact_9dd7f790adcb8674", + "artifact_type": "unknown", + "display_name": "logout", + "locations": [ + { + "path": "app.py", + "line": 60, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_43dfb90d25264dc4" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "fastapi" + ] + }, + { + "id": "artifact_9a37039751082f3f", + "artifact_type": "unknown", + "display_name": "security_dependency", + "locations": [ + { + "path": "app.py", + "line": 60, + "column": 37 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [ + "evidence_c5dd88fd06c26093" + ], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "fastapi" + ] + }, + { + "id": "artifact_345218b3714d0c8a", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "app.py", + "line": 61, + "column": 5 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_c0a93958834374e4" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "python" + ] + }, + { + "id": "artifact_26239a282b5467e3", + "artifact_type": "session_cookie", + "display_name": "session", + "locations": [ + { + "path": "app.py", + "line": 62, + "column": 5 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_41eb635462b4ba20" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "python" + ] + } + ], + "evidence": [ + { + "id": "evidence_67ba10e56375217d", + "lifecycle_stage": "issue", + "location": { + "path": "app.py", + "line": 14, + "column": 12 + }, + "detector_id": "jwt.issue", + "confidence": "high", + "excerpt": "pyjwt.encode issue call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1d120b1c0c19e112", + "lifecycle_stage": "issue", + "location": { + "path": "app.py", + "line": 16, + "column": 20 + }, + "detector_id": "jwt.attribute.subject", + "confidence": "high", + "excerpt": "subject claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_485ff7f12a7a83f8", + "lifecycle_stage": "issue", + "location": { + "path": "app.py", + "line": 17, + "column": 20 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "ISSUER", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_b1e065af58c3925c", + "lifecycle_stage": "issue", + "location": { + "path": "app.py", + "line": 18, + "column": 20 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "AUDIENCE", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_dd7490cd811b1d0c", + "lifecycle_stage": "expire", + "location": { + "path": "app.py", + "line": 19, + "column": 20 + }, + "detector_id": "jwt.attribute.expiration", + "confidence": "high", + "excerpt": "expires_at", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_98eb78b1628dff5a", + "lifecycle_stage": "issue", + "location": { + "path": "app.py", + "line": 21, + "column": 9 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_04e27dc796edd948", + "lifecycle_stage": "issue", + "location": { + "path": "app.py", + "line": 22, + "column": 19 + }, + "detector_id": "jwt.attribute.algorithm", + "confidence": "high", + "excerpt": "\"HS256\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_400f7ea0d2ddb252", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 29, + "column": 12 + }, + "detector_id": "jwt.attribute.expiry_enforcement", + "confidence": "low", + "excerpt": "PyJWT decode enforces exp by library default", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_6b8e2bb85b4a5c59", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 29, + "column": 12 + }, + "detector_id": "jwt.attribute.signature_verification", + "confidence": "high", + "excerpt": "PyJWT decode verifies signatures when a key is supplied", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_b61b525c331695f4", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 29, + "column": 12 + }, + "detector_id": "jwt.validate", + "confidence": "high", + "excerpt": "pyjwt.decode validate call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_879be86c887ffc25", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 31, + "column": 9 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9ec068fd376b7d89", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 32, + "column": 20 + }, + "detector_id": "jwt.attribute.algorithm", + "confidence": "high", + "excerpt": "[\"HS256\"]", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_8251d59500cb750b", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 32, + "column": 20 + }, + "detector_id": "jwt.option.algorithms", + "confidence": "high", + "excerpt": "[\"HS256\"]", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_0430950bd7d6b709", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 33, + "column": 16 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "ISSUER", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_67f9caad64bf13a6", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 33, + "column": 16 + }, + "detector_id": "jwt.option.issuer", + "confidence": "high", + "excerpt": "JWT issuer option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_751429b89c10824f", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 34, + "column": 18 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "AUDIENCE", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1a96e0988bbbf340", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 34, + "column": 18 + }, + "detector_id": "jwt.option.audience", + "confidence": "high", + "excerpt": "JWT audience option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_bab2541ae5986d41", + "lifecycle_stage": "issue", + "location": { + "path": "app.py", + "line": 39, + "column": 1 + }, + "detector_id": "session.auth_transition", + "confidence": "high", + "excerpt": "def login(response: Response):\n token = issue_access_token(\"[REDACTED]\")\n response.set_cookie(\n \"[REDACTED]\",\n token,\n httponly=True,\n secure=True,\n samesite=\"[REDACTED]\",\n max_age=900,\n )\n return {\"[REDACTED]\": True}", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_0277e4a57e2be5d0", + "lifecycle_stage": "store", + "location": { + "path": "app.py", + "line": 40, + "column": 5 + }, + "detector_id": "bearer.store.config", + "confidence": "high", + "excerpt": "token = issue_access_token(\"[REDACTED]\")", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_8f65a9c03ac6856e", + "lifecycle_stage": "introspect", + "location": { + "path": "app.py", + "line": 40, + "column": 5 + }, + "detector_id": "bearer.boundary.issuer", + "confidence": "high", + "excerpt": "token = issue_access_token(\"[REDACTED]\")", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_d40d72776f6ad953", + "lifecycle_stage": "introspect", + "location": { + "path": "app.py", + "line": 40, + "column": 13 + }, + "detector_id": "bearer.boundary.issuer", + "confidence": "high", + "excerpt": "issue_access_token(\"[REDACTED]\")", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_ee55e24a0d65191a", + "lifecycle_stage": "store", + "location": { + "path": "app.py", + "line": 41, + "column": 5 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_12991725538e196c", + "lifecycle_stage": "store", + "location": { + "path": "app.py", + "line": 41, + "column": 5 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": " [REDACTED] = issue_access_[REDACTED](\"placeholder-user\")\n response.set_cookie(\n \"session\",", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_063c471d6ac166fc", + "lifecycle_stage": "transmit", + "location": { + "path": "app.py", + "line": 41, + "column": 5 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "low", + "excerpt": "Domain defaults to none for python", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_427e3aca09f2a901", + "lifecycle_stage": "transmit", + "location": { + "path": "app.py", + "line": 41, + "column": 5 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for python", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_a7c8930136f027d4", + "lifecycle_stage": "expire", + "location": { + "path": "app.py", + "line": 41, + "column": 5 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "low", + "excerpt": "Expires defaults to none for python", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_2e869cc62fb447f5", + "lifecycle_stage": "store", + "location": { + "path": "app.py", + "line": 44, + "column": 18 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "HttpOnly: True", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_61c3d2780025a546", + "lifecycle_stage": "transmit", + "location": { + "path": "app.py", + "line": 45, + "column": 16 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "Secure: True", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_4b35d34e5e07ffc4", + "lifecycle_stage": "transmit", + "location": { + "path": "app.py", + "line": 46, + "column": 18 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SameSite: \"lax\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_51e68c6ad8120fd6", + "lifecycle_stage": "expire", + "location": { + "path": "app.py", + "line": 47, + "column": 17 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "high", + "excerpt": "Max-Age: 900", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_7e0f8ab5f17e8838", + "lifecycle_stage": "expire", + "location": { + "path": "app.py", + "line": 55, + "column": 5 + }, + "detector_id": "bearer.expire", + "confidence": "high", + "excerpt": "store_reset_token(\"[REDACTED]\", \"[REDACTED]\", expires_at)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_43dfb90d25264dc4", + "lifecycle_stage": "revoke", + "location": { + "path": "app.py", + "line": 60, + "column": 1 + }, + "detector_id": "logout.handler", + "confidence": "high", + "excerpt": "def logout(response: Response, user=Depends(current_user)):\n revoke_session(user[\"[REDACTED]\"])\n response.delete_cookie(\"[REDACTED]\")\n return Response(status_code=204)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c5dd88fd06c26093", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 60, + "column": 37 + }, + "detector_id": "fastapi.security_dependency", + "confidence": "medium", + "excerpt": "Depends(current_user)", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_c0a93958834374e4", + "lifecycle_stage": "revoke", + "location": { + "path": "app.py", + "line": 61, + "column": 5 + }, + "detector_id": "logout.session_destroy", + "confidence": "high", + "excerpt": "revoke_session(user[\"[REDACTED]\"])", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_41eb635462b4ba20", + "lifecycle_stage": "revoke", + "location": { + "path": "app.py", + "line": 62, + "column": 5 + }, + "detector_id": "logout.cookie_clear", + "confidence": "high", + "excerpt": "response.delete_cookie(\"session\")", + "dynamic": false, + "framework_default": false + } + ], + "diagnostics": [], + "skipped_reason": null + }, + { + "path": "expected.json", + "language": "json", + "artifacts": [], + "evidence": [], + "diagnostics": [], + "skipped_reason": null + } + ], + "artifacts": [ + { + "id": "artifact_1f6194ee35663737", + "artifact_type": "access_jwt", + "display_name": "access_jwt", + "locations": [ + { + "path": "app.py", + "line": 14, + "column": 12 + }, + { + "path": "app.py", + "line": 29, + "column": 12 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_04e27dc796edd948", + "evidence_1d120b1c0c19e112", + "evidence_485ff7f12a7a83f8", + "evidence_67ba10e56375217d", + "evidence_98eb78b1628dff5a", + "evidence_b1e065af58c3925c" + ], + "store": [], + "transmit": [], + "validate": [ + "evidence_0430950bd7d6b709", + "evidence_1a96e0988bbbf340", + "evidence_400f7ea0d2ddb252", + "evidence_67f9caad64bf13a6", + "evidence_6b8e2bb85b4a5c59", + "evidence_751429b89c10824f", + "evidence_8251d59500cb750b", + "evidence_879be86c887ffc25", + "evidence_9ec068fd376b7d89", + "evidence_b61b525c331695f4" + ], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_dd7490cd811b1d0c" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "pyjwt" + ], + "jwt_attributes": { + "operation": { + "state": "present", + "value": "issue, validate", + "evidence_ids": [ + "evidence_67ba10e56375217d", + "evidence_b61b525c331695f4" + ], + "confidence": "high" + }, + "algorithm": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_04e27dc796edd948", + "evidence_9ec068fd376b7d89" + ], + "confidence": "high" + }, + "key_reference": { + "state": "present", + "value": "JWT_SECRET", + "evidence_ids": [ + "evidence_879be86c887ffc25", + "evidence_98eb78b1628dff5a" + ], + "confidence": "high" + }, + "issuer": { + "state": "present", + "value": "ISSUER", + "evidence_ids": [ + "evidence_0430950bd7d6b709", + "evidence_485ff7f12a7a83f8" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_751429b89c10824f", + "evidence_b1e065af58c3925c" + ], + "confidence": "high" + }, + "expiration": { + "state": "present", + "value": "expires_at", + "evidence_ids": [ + "evidence_dd7490cd811b1d0c" + ], + "confidence": "high" + }, + "signature_verification": { + "state": "present", + "value": "verified", + "evidence_ids": [ + "evidence_6b8e2bb85b4a5c59" + ], + "confidence": "high" + }, + "expiry_enforcement": { + "state": "framework_default", + "value": "PyJWT decode default", + "evidence_ids": [ + "evidence_400f7ea0d2ddb252" + ], + "confidence": "low" + }, + "identity_claims": { + "subject": { + "state": "present", + "value": "user_id", + "evidence_ids": [ + "evidence_1d120b1c0c19e112" + ], + "confidence": "high" + }, + "user_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "tenant_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "org_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "workspace_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "roles": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "scopes": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "groups": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "email": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "email_verified": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "auth_method": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "auth_class": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + } + } + }, + "token_boundary_attributes": { + "issuer": { + "state": "present", + "value": "ISSUER", + "evidence_ids": [ + "evidence_0430950bd7d6b709", + "evidence_485ff7f12a7a83f8" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_751429b89c10824f", + "evidence_b1e065af58c3925c" + ], + "confidence": "high" + }, + "trust_boundary": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_751429b89c10824f", + "evidence_b1e065af58c3925c" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_6fe7bd9aa9441210", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "app.py", + "line": 39, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_bab2541ae5986d41" + ], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "fastapi", + "scope:898" + ] + }, + { + "id": "artifact_6662c62fe10196d2", + "artifact_type": "opaque_bearer_token", + "display_name": "access_token", + "locations": [ + { + "path": "app.py", + "line": 40, + "column": 5 + }, + { + "path": "app.py", + "line": 40, + "column": 13 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_0277e4a57e2be5d0" + ], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [ + "evidence_8f65a9c03ac6856e", + "evidence_d40d72776f6ad953" + ] + }, + "confidence": "high", + "framework_hints": [ + "python" + ], + "token_boundary_attributes": { + "issuer": { + "state": "present", + "evidence_ids": [ + "evidence_8f65a9c03ac6856e", + "evidence_d40d72776f6ad953" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_12991725538e196c", + "artifact_type": "session_cookie", + "display_name": "session", + "locations": [ + { + "path": "app.py", + "line": 41, + "column": 5 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_12991725538e196c", + "evidence_2e869cc62fb447f5", + "evidence_ee55e24a0d65191a" + ], + "transmit": [ + "evidence_063c471d6ac166fc", + "evidence_427e3aca09f2a901", + "evidence_4b35d34e5e07ffc4", + "evidence_61c3d2780025a546" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_51e68c6ad8120fd6", + "evidence_a7c8930136f027d4" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "python" + ], + "cookie_attributes": { + "http_only": { + "state": "present", + "value": "True", + "evidence_ids": [ + "evidence_2e869cc62fb447f5" + ], + "confidence": "high" + }, + "secure": { + "state": "present", + "value": "True", + "evidence_ids": [ + "evidence_61c3d2780025a546" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "lax", + "evidence_ids": [ + "evidence_4b35d34e5e07ffc4" + ], + "confidence": "high" + }, + "max_age": { + "state": "present", + "value": "900", + "evidence_ids": [ + "evidence_51e68c6ad8120fd6" + ], + "confidence": "high" + }, + "expires": { + "state": "framework_default", + "value": "none", + "evidence_ids": [ + "evidence_a7c8930136f027d4" + ], + "confidence": "low" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_427e3aca09f2a901" + ], + "confidence": "low" + }, + "domain": { + "state": "framework_default", + "value": "none", + "evidence_ids": [ + "evidence_063c471d6ac166fc" + ], + "confidence": "low" + } + } + }, + { + "id": "artifact_35be80ef8141f7cb", + "artifact_type": "unknown_token", + "display_name": "unknown_token", + "locations": [ + { + "path": "app.py", + "line": 55, + "column": 5 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_7e0f8ab5f17e8838" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "python" + ] + }, + { + "id": "artifact_9dd7f790adcb8674", + "artifact_type": "unknown", + "display_name": "logout", + "locations": [ + { + "path": "app.py", + "line": 60, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_43dfb90d25264dc4" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "fastapi" + ] + }, + { + "id": "artifact_9a37039751082f3f", + "artifact_type": "unknown", + "display_name": "security_dependency", + "locations": [ + { + "path": "app.py", + "line": 60, + "column": 37 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [ + "evidence_c5dd88fd06c26093" + ], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "fastapi" + ] + }, + { + "id": "artifact_345218b3714d0c8a", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "app.py", + "line": 61, + "column": 5 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_c0a93958834374e4" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "python" + ] + }, + { + "id": "artifact_26239a282b5467e3", + "artifact_type": "session_cookie", + "display_name": "session", + "locations": [ + { + "path": "app.py", + "line": 62, + "column": 5 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_41eb635462b4ba20" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "python" + ] + } + ], + "evidence": [ + { + "id": "evidence_67ba10e56375217d", + "lifecycle_stage": "issue", + "location": { + "path": "app.py", + "line": 14, + "column": 12 + }, + "detector_id": "jwt.issue", + "confidence": "high", + "excerpt": "pyjwt.encode issue call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1d120b1c0c19e112", + "lifecycle_stage": "issue", + "location": { + "path": "app.py", + "line": 16, + "column": 20 + }, + "detector_id": "jwt.attribute.subject", + "confidence": "high", + "excerpt": "subject claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_485ff7f12a7a83f8", + "lifecycle_stage": "issue", + "location": { + "path": "app.py", + "line": 17, + "column": 20 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "ISSUER", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_b1e065af58c3925c", + "lifecycle_stage": "issue", + "location": { + "path": "app.py", + "line": 18, + "column": 20 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "AUDIENCE", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_dd7490cd811b1d0c", + "lifecycle_stage": "expire", + "location": { + "path": "app.py", + "line": 19, + "column": 20 + }, + "detector_id": "jwt.attribute.expiration", + "confidence": "high", + "excerpt": "expires_at", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_98eb78b1628dff5a", + "lifecycle_stage": "issue", + "location": { + "path": "app.py", + "line": 21, + "column": 9 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_04e27dc796edd948", + "lifecycle_stage": "issue", + "location": { + "path": "app.py", + "line": 22, + "column": 19 + }, + "detector_id": "jwt.attribute.algorithm", + "confidence": "high", + "excerpt": "\"HS256\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_400f7ea0d2ddb252", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 29, + "column": 12 + }, + "detector_id": "jwt.attribute.expiry_enforcement", + "confidence": "low", + "excerpt": "PyJWT decode enforces exp by library default", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_6b8e2bb85b4a5c59", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 29, + "column": 12 + }, + "detector_id": "jwt.attribute.signature_verification", + "confidence": "high", + "excerpt": "PyJWT decode verifies signatures when a key is supplied", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_b61b525c331695f4", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 29, + "column": 12 + }, + "detector_id": "jwt.validate", + "confidence": "high", + "excerpt": "pyjwt.decode validate call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_879be86c887ffc25", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 31, + "column": 9 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9ec068fd376b7d89", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 32, + "column": 20 + }, + "detector_id": "jwt.attribute.algorithm", + "confidence": "high", + "excerpt": "[\"HS256\"]", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_8251d59500cb750b", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 32, + "column": 20 + }, + "detector_id": "jwt.option.algorithms", + "confidence": "high", + "excerpt": "[\"HS256\"]", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_0430950bd7d6b709", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 33, + "column": 16 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "ISSUER", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_67f9caad64bf13a6", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 33, + "column": 16 + }, + "detector_id": "jwt.option.issuer", + "confidence": "high", + "excerpt": "JWT issuer option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_751429b89c10824f", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 34, + "column": 18 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "AUDIENCE", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1a96e0988bbbf340", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 34, + "column": 18 + }, + "detector_id": "jwt.option.audience", + "confidence": "high", + "excerpt": "JWT audience option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_bab2541ae5986d41", + "lifecycle_stage": "issue", + "location": { + "path": "app.py", + "line": 39, + "column": 1 + }, + "detector_id": "session.auth_transition", + "confidence": "high", + "excerpt": "def login(response: Response):\n token = issue_access_token(\"[REDACTED]\")\n response.set_cookie(\n \"[REDACTED]\",\n token,\n httponly=True,\n secure=True,\n samesite=\"[REDACTED]\",\n max_age=900,\n )\n return {\"[REDACTED]\": True}", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_0277e4a57e2be5d0", + "lifecycle_stage": "store", + "location": { + "path": "app.py", + "line": 40, + "column": 5 + }, + "detector_id": "bearer.store.config", + "confidence": "high", + "excerpt": "token = issue_access_token(\"[REDACTED]\")", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_8f65a9c03ac6856e", + "lifecycle_stage": "introspect", + "location": { + "path": "app.py", + "line": 40, + "column": 5 + }, + "detector_id": "bearer.boundary.issuer", + "confidence": "high", + "excerpt": "token = issue_access_token(\"[REDACTED]\")", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_d40d72776f6ad953", + "lifecycle_stage": "introspect", + "location": { + "path": "app.py", + "line": 40, + "column": 13 + }, + "detector_id": "bearer.boundary.issuer", + "confidence": "high", + "excerpt": "issue_access_token(\"[REDACTED]\")", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_ee55e24a0d65191a", + "lifecycle_stage": "store", + "location": { + "path": "app.py", + "line": 41, + "column": 5 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_12991725538e196c", + "lifecycle_stage": "store", + "location": { + "path": "app.py", + "line": 41, + "column": 5 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": " [REDACTED] = issue_access_[REDACTED](\"placeholder-user\")\n response.set_cookie(\n \"session\",", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_063c471d6ac166fc", + "lifecycle_stage": "transmit", + "location": { + "path": "app.py", + "line": 41, + "column": 5 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "low", + "excerpt": "Domain defaults to none for python", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_427e3aca09f2a901", + "lifecycle_stage": "transmit", + "location": { + "path": "app.py", + "line": 41, + "column": 5 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for python", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_a7c8930136f027d4", + "lifecycle_stage": "expire", + "location": { + "path": "app.py", + "line": 41, + "column": 5 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "low", + "excerpt": "Expires defaults to none for python", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_2e869cc62fb447f5", + "lifecycle_stage": "store", + "location": { + "path": "app.py", + "line": 44, + "column": 18 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "HttpOnly: True", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_61c3d2780025a546", + "lifecycle_stage": "transmit", + "location": { + "path": "app.py", + "line": 45, + "column": 16 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "Secure: True", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_4b35d34e5e07ffc4", + "lifecycle_stage": "transmit", + "location": { + "path": "app.py", + "line": 46, + "column": 18 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SameSite: \"lax\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_51e68c6ad8120fd6", + "lifecycle_stage": "expire", + "location": { + "path": "app.py", + "line": 47, + "column": 17 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "high", + "excerpt": "Max-Age: 900", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_7e0f8ab5f17e8838", + "lifecycle_stage": "expire", + "location": { + "path": "app.py", + "line": 55, + "column": 5 + }, + "detector_id": "bearer.expire", + "confidence": "high", + "excerpt": "store_reset_token(\"[REDACTED]\", \"[REDACTED]\", expires_at)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_43dfb90d25264dc4", + "lifecycle_stage": "revoke", + "location": { + "path": "app.py", + "line": 60, + "column": 1 + }, + "detector_id": "logout.handler", + "confidence": "high", + "excerpt": "def logout(response: Response, user=Depends(current_user)):\n revoke_session(user[\"[REDACTED]\"])\n response.delete_cookie(\"[REDACTED]\")\n return Response(status_code=204)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c5dd88fd06c26093", + "lifecycle_stage": "validate", + "location": { + "path": "app.py", + "line": 60, + "column": 37 + }, + "detector_id": "fastapi.security_dependency", + "confidence": "medium", + "excerpt": "Depends(current_user)", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_c0a93958834374e4", + "lifecycle_stage": "revoke", + "location": { + "path": "app.py", + "line": 61, + "column": 5 + }, + "detector_id": "logout.session_destroy", + "confidence": "high", + "excerpt": "revoke_session(user[\"[REDACTED]\"])", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_41eb635462b4ba20", + "lifecycle_stage": "revoke", + "location": { + "path": "app.py", + "line": 62, + "column": 5 + }, + "detector_id": "logout.cookie_clear", + "confidence": "high", + "excerpt": "response.delete_cookie(\"session\")", + "dynamic": false, + "framework_default": false + } + ], + "lifecycle_paths": [ + { + "id": "lifecycle_path_f8d48d8053a90bb6", + "artifact_ids": [ + "artifact_12991725538e196c", + "artifact_26239a282b5467e3", + "artifact_345218b3714d0c8a" + ], + "stages": [ + { + "stage": "store", + "evidence_ids": [ + "evidence_12991725538e196c", + "evidence_2e869cc62fb447f5", + "evidence_ee55e24a0d65191a" + ] + }, + { + "stage": "transmit", + "evidence_ids": [ + "evidence_063c471d6ac166fc", + "evidence_427e3aca09f2a901", + "evidence_4b35d34e5e07ffc4", + "evidence_61c3d2780025a546" + ] + }, + { + "stage": "revoke", + "evidence_ids": [ + "evidence_41eb635462b4ba20", + "evidence_c0a93958834374e4" + ] + }, + { + "stage": "expire", + "evidence_ids": [ + "evidence_51e68c6ad8120fd6", + "evidence_a7c8930136f027d4" + ] + } + ], + "confidence": "low", + "dynamic": false, + "reviewer_question": "Which framework version and deployment settings determine lifecycle behavior for `session`?" + }, + { + "id": "lifecycle_path_7245767eb9cf69d6", + "artifact_ids": [ + "artifact_1f6194ee35663737" + ], + "stages": [ + { + "stage": "issue", + "evidence_ids": [ + "evidence_04e27dc796edd948", + "evidence_1d120b1c0c19e112", + "evidence_485ff7f12a7a83f8", + "evidence_67ba10e56375217d", + "evidence_98eb78b1628dff5a", + "evidence_b1e065af58c3925c" + ] + }, + { + "stage": "validate", + "evidence_ids": [ + "evidence_0430950bd7d6b709", + "evidence_1a96e0988bbbf340", + "evidence_400f7ea0d2ddb252", + "evidence_67f9caad64bf13a6", + "evidence_6b8e2bb85b4a5c59", + "evidence_751429b89c10824f", + "evidence_8251d59500cb750b", + "evidence_879be86c887ffc25", + "evidence_9ec068fd376b7d89", + "evidence_b61b525c331695f4" + ] + }, + { + "stage": "expire", + "evidence_ids": [ + "evidence_dd7490cd811b1d0c" + ] + } + ], + "confidence": "low", + "dynamic": false, + "reviewer_question": "Which framework version and deployment settings determine lifecycle behavior for `access_jwt`?" + }, + { + "id": "lifecycle_path_9bd8d97cc0befed7", + "artifact_ids": [ + "artifact_35be80ef8141f7cb" + ], + "stages": [ + { + "stage": "expire", + "evidence_ids": [ + "evidence_7e0f8ab5f17e8838" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_d31556a0b43c2904", + "artifact_ids": [ + "artifact_6662c62fe10196d2" + ], + "stages": [ + { + "stage": "store", + "evidence_ids": [ + "evidence_0277e4a57e2be5d0" + ] + }, + { + "stage": "introspect", + "evidence_ids": [ + "evidence_8f65a9c03ac6856e", + "evidence_d40d72776f6ad953" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_16d22fa8a59e37e3", + "artifact_ids": [ + "artifact_6fe7bd9aa9441210" + ], + "stages": [ + { + "stage": "issue", + "evidence_ids": [ + "evidence_bab2541ae5986d41" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_29d30d2dc0b5cd9c", + "artifact_ids": [ + "artifact_9a37039751082f3f" + ], + "stages": [ + { + "stage": "validate", + "evidence_ids": [ + "evidence_c5dd88fd06c26093" + ] + } + ], + "confidence": "medium", + "dynamic": true, + "reviewer_question": "Which production code path determines the effective lifecycle behavior for `security_dependency`?" + }, + { + "id": "lifecycle_path_c9b761a2e59e2fbd", + "artifact_ids": [ + "artifact_9dd7f790adcb8674" + ], + "stages": [ + { + "stage": "revoke", + "evidence_ids": [ + "evidence_43dfb90d25264dc4" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + } + ], + "findings": [ + { + "id": "finding_123f9760ef48ff07", + "category": "lifecycle_gap", + "severity": "medium", + "artifact_ids": [ + "artifact_1f6194ee35663737" + ], + "evidence_ids": [ + "evidence_04e27dc796edd948", + "evidence_1d120b1c0c19e112", + "evidence_43dfb90d25264dc4", + "evidence_485ff7f12a7a83f8", + "evidence_67ba10e56375217d", + "evidence_98eb78b1628dff5a", + "evidence_b1e065af58c3925c" + ], + "title": "JWT `access_jwt` has logout evidence without linked denylist evidence", + "description": "A logout handler and access-JWT lifecycle evidence were detected in linked source context, but no source-bound denylist, blocklist, or token revocation-store insertion evidence was linked for the same logout flow.", + "suggested_fix": "Insert the JWT identifier into a denylist/blocklist or revoke-store on logout, or document the intentional short-TTL stateless model for reviewer confirmation.", + "reviewer_question": "Where does logout revoke or denylist outstanding `access_jwt` tokens?" + }, + { + "id": "finding_579fa11bcc87ed34", + "category": "dynamic_review_required", + "severity": "medium", + "artifact_ids": [ + "artifact_6fe7bd9aa9441210" + ], + "evidence_ids": [ + "evidence_bab2541ae5986d41" + ], + "title": "Session regeneration evidence was not found near login", + "description": "An authentication transition was detected, but nearby static evidence did not show an explicit session regeneration, cookie reissue, or recognized framework-default rotation point.", + "suggested_fix": "Identify the framework's session rotation primitive and call it during authentication and privilege transitions.", + "reviewer_question": "Where is the session identifier rotated after this authentication transition?" + }, + { + "id": "finding_c13478d416e9902b", + "category": "missing_validation_evidence", + "severity": "low", + "artifact_ids": [ + "artifact_1f6194ee35663737" + ], + "evidence_ids": [ + "evidence_b61b525c331695f4", + "evidence_9ec068fd376b7d89", + "evidence_879be86c887ffc25", + "evidence_0430950bd7d6b709", + "evidence_751429b89c10824f", + "evidence_6b8e2bb85b4a5c59", + "evidence_400f7ea0d2ddb252", + "evidence_8251d59500cb750b", + "evidence_1a96e0988bbbf340", + "evidence_67f9caad64bf13a6" + ], + "title": "JWT `access_jwt` verification has no not-before validation evidence", + "description": "JWT verification evidence does not show `nbf` / not-before claim enforcement.", + "suggested_fix": "Require or enforce `nbf` validation where issuer policy uses not-before claims.", + "reviewer_question": "Do issued tokens for this path use `nbf`, and is it enforced in production?" + }, + { + "id": "finding_570c03dd66d92170", + "category": "dynamic_review_required", + "severity": "low", + "artifact_ids": [ + "artifact_6662c62fe10196d2" + ], + "evidence_ids": [ + "evidence_0277e4a57e2be5d0" + ], + "title": "Token `access_token` has provider-managed or dynamic handling", + "description": "Bearer/API-key behavior appears dynamic, provider-managed, config-driven, or server-to-server only, so the static evidence should be reviewed before treating lifecycle controls as deterministic.", + "suggested_fix": "Document the provider, wrapper, or server-side policy for issuance, storage, validation, scope, expiry, rotation, and revocation.", + "reviewer_question": "Which runtime configuration or provider settings govern this token lifecycle?" + }, + { + "id": "finding_07dfd3c55c8e74d9", + "category": "dynamic_review_required", + "severity": "low", + "artifact_ids": [ + "artifact_6662c62fe10196d2" + ], + "evidence_ids": [ + "evidence_8f65a9c03ac6856e", + "evidence_d40d72776f6ad953" + ], + "title": "Token `access_token` has boundary metadata without visible constraints", + "description": "Issuer, provider, or trust-boundary context was detected without nearby audience, service, tenant, or scope constraint evidence.", + "suggested_fix": "Document or expose the concrete audience, service, tenant, or scope policy that constrains this token.", + "reviewer_question": "Which local or provider policy prevents this token from being reused outside the intended boundary?" + }, + { + "id": "finding_51328750744a1db3", + "category": "framework_default_assumed", + "severity": "low", + "artifact_ids": [ + "artifact_1f6194ee35663737" + ], + "evidence_ids": [ + "evidence_400f7ea0d2ddb252" + ], + "title": "JWT `access_jwt` expiry enforcement relies on library defaults", + "description": "JWT validation appears to rely on the library default for expiration enforcement.", + "suggested_fix": "Make expiration enforcement explicit or document the library version and default.", + "reviewer_question": "Which JWT library version and settings determine expiration enforcement here?" + } + ] +} \ No newline at end of file diff --git a/tests/integration/snapshots/generic-js-jwt-crypto-trust-alg-none.json b/tests/integration/snapshots/generic-js-jwt-crypto-trust-alg-none.json new file mode 100644 index 0000000..7736125 --- /dev/null +++ b/tests/integration/snapshots/generic-js-jwt-crypto-trust-alg-none.json @@ -0,0 +1,658 @@ +{ + "schema_version": "0.5.0", + "summary": { + "files_discovered": 2, + "files_scanned": 2, + "files_skipped": 0, + "diagnostics": [], + "worker_panic_count": 0 + }, + "files": [ + { + "path": "auth.js", + "language": "java_script", + "artifacts": [ + { + "id": "artifact_3be9a9359fa26462", + "artifact_type": "access_jwt", + "display_name": "access_jwt", + "locations": [ + { + "path": "auth.js", + "line": 8, + "column": 10 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [ + "evidence_19b19b3ff8c58764", + "evidence_2d5339153a64426a", + "evidence_32f7bf5d08e4949e", + "evidence_71052ce1d63e5977", + "evidence_7491e2691c2a91d9", + "evidence_756ae30fbb990830", + "evidence_8b4dac52bdc5a9a1", + "evidence_9faf5bd09db59388", + "evidence_cc285f22f612f773", + "evidence_d4b59bd0fc8ab376", + "evidence_e9efd84e48d00ea0" + ], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "jsonwebtoken" + ], + "jwt_attributes": { + "operation": { + "state": "present", + "value": "validate", + "evidence_ids": [ + "evidence_32f7bf5d08e4949e" + ], + "confidence": "high" + }, + "algorithm": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_19b19b3ff8c58764" + ], + "confidence": "high" + }, + "key_reference": { + "state": "present", + "value": "PUBLIC_KEY", + "evidence_ids": [ + "evidence_cc285f22f612f773" + ], + "confidence": "high" + }, + "issuer": { + "state": "present", + "value": "ISSUER", + "evidence_ids": [ + "evidence_9faf5bd09db59388" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_d4b59bd0fc8ab376" + ], + "confidence": "high" + }, + "expiration": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "signature_verification": { + "state": "present", + "value": "verified", + "evidence_ids": [ + "evidence_7491e2691c2a91d9" + ], + "confidence": "high" + }, + "expiry_enforcement": { + "state": "framework_default", + "value": "jsonwebtoken.verify default", + "evidence_ids": [ + "evidence_756ae30fbb990830" + ], + "confidence": "low" + } + }, + "token_boundary_attributes": { + "issuer": { + "state": "present", + "value": "ISSUER", + "evidence_ids": [ + "evidence_9faf5bd09db59388" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_d4b59bd0fc8ab376" + ], + "confidence": "high" + }, + "trust_boundary": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_d4b59bd0fc8ab376" + ], + "confidence": "high" + } + } + } + ], + "evidence": [ + { + "id": "evidence_756ae30fbb990830", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 8, + "column": 10 + }, + "detector_id": "jwt.attribute.expiry_enforcement", + "confidence": "low", + "excerpt": "jsonwebtoken.verify enforces exp by library default", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_7491e2691c2a91d9", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 8, + "column": 10 + }, + "detector_id": "jwt.attribute.signature_verification", + "confidence": "high", + "excerpt": "jsonwebtoken.verify verifies JWT signatures", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_32f7bf5d08e4949e", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 8, + "column": 10 + }, + "detector_id": "jwt.validate", + "confidence": "high", + "excerpt": "jsonwebtoken.verify validate call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_cc285f22f612f773", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 8, + "column": 28 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_19b19b3ff8c58764", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 9, + "column": 17 + }, + "detector_id": "jwt.attribute.algorithm", + "confidence": "high", + "excerpt": "[\"none\"]", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2d5339153a64426a", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 9, + "column": 17 + }, + "detector_id": "jwt.option.algorithms", + "confidence": "high", + "excerpt": "[\"none\"]", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9faf5bd09db59388", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 10, + "column": 13 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "ISSUER", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_71052ce1d63e5977", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 10, + "column": 13 + }, + "detector_id": "jwt.option.issuer", + "confidence": "high", + "excerpt": "JWT issuer option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_d4b59bd0fc8ab376", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 11, + "column": 15 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "AUDIENCE", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_8b4dac52bdc5a9a1", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 11, + "column": 15 + }, + "detector_id": "jwt.option.audience", + "confidence": "high", + "excerpt": "JWT audience option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_e9efd84e48d00ea0", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 12, + "column": 22 + }, + "detector_id": "jwt.option.ignore_not_before", + "confidence": "high", + "excerpt": "false", + "dynamic": false, + "framework_default": false + } + ], + "diagnostics": [], + "skipped_reason": null + }, + { + "path": "expected.json", + "language": "json", + "artifacts": [], + "evidence": [], + "diagnostics": [], + "skipped_reason": null + } + ], + "artifacts": [ + { + "id": "artifact_3be9a9359fa26462", + "artifact_type": "access_jwt", + "display_name": "access_jwt", + "locations": [ + { + "path": "auth.js", + "line": 8, + "column": 10 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [ + "evidence_19b19b3ff8c58764", + "evidence_2d5339153a64426a", + "evidence_32f7bf5d08e4949e", + "evidence_71052ce1d63e5977", + "evidence_7491e2691c2a91d9", + "evidence_756ae30fbb990830", + "evidence_8b4dac52bdc5a9a1", + "evidence_9faf5bd09db59388", + "evidence_cc285f22f612f773", + "evidence_d4b59bd0fc8ab376", + "evidence_e9efd84e48d00ea0" + ], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "jsonwebtoken" + ], + "jwt_attributes": { + "operation": { + "state": "present", + "value": "validate", + "evidence_ids": [ + "evidence_32f7bf5d08e4949e" + ], + "confidence": "high" + }, + "algorithm": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_19b19b3ff8c58764" + ], + "confidence": "high" + }, + "key_reference": { + "state": "present", + "value": "PUBLIC_KEY", + "evidence_ids": [ + "evidence_cc285f22f612f773" + ], + "confidence": "high" + }, + "issuer": { + "state": "present", + "value": "ISSUER", + "evidence_ids": [ + "evidence_9faf5bd09db59388" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_d4b59bd0fc8ab376" + ], + "confidence": "high" + }, + "expiration": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "signature_verification": { + "state": "present", + "value": "verified", + "evidence_ids": [ + "evidence_7491e2691c2a91d9" + ], + "confidence": "high" + }, + "expiry_enforcement": { + "state": "framework_default", + "value": "jsonwebtoken.verify default", + "evidence_ids": [ + "evidence_756ae30fbb990830" + ], + "confidence": "low" + } + }, + "token_boundary_attributes": { + "issuer": { + "state": "present", + "value": "ISSUER", + "evidence_ids": [ + "evidence_9faf5bd09db59388" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_d4b59bd0fc8ab376" + ], + "confidence": "high" + }, + "trust_boundary": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_d4b59bd0fc8ab376" + ], + "confidence": "high" + } + } + } + ], + "evidence": [ + { + "id": "evidence_756ae30fbb990830", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 8, + "column": 10 + }, + "detector_id": "jwt.attribute.expiry_enforcement", + "confidence": "low", + "excerpt": "jsonwebtoken.verify enforces exp by library default", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_7491e2691c2a91d9", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 8, + "column": 10 + }, + "detector_id": "jwt.attribute.signature_verification", + "confidence": "high", + "excerpt": "jsonwebtoken.verify verifies JWT signatures", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_32f7bf5d08e4949e", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 8, + "column": 10 + }, + "detector_id": "jwt.validate", + "confidence": "high", + "excerpt": "jsonwebtoken.verify validate call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_cc285f22f612f773", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 8, + "column": 28 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_19b19b3ff8c58764", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 9, + "column": 17 + }, + "detector_id": "jwt.attribute.algorithm", + "confidence": "high", + "excerpt": "[\"none\"]", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2d5339153a64426a", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 9, + "column": 17 + }, + "detector_id": "jwt.option.algorithms", + "confidence": "high", + "excerpt": "[\"none\"]", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9faf5bd09db59388", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 10, + "column": 13 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "ISSUER", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_71052ce1d63e5977", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 10, + "column": 13 + }, + "detector_id": "jwt.option.issuer", + "confidence": "high", + "excerpt": "JWT issuer option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_d4b59bd0fc8ab376", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 11, + "column": 15 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "AUDIENCE", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_8b4dac52bdc5a9a1", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 11, + "column": 15 + }, + "detector_id": "jwt.option.audience", + "confidence": "high", + "excerpt": "JWT audience option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_e9efd84e48d00ea0", + "lifecycle_stage": "validate", + "location": { + "path": "auth.js", + "line": 12, + "column": 22 + }, + "detector_id": "jwt.option.ignore_not_before", + "confidence": "high", + "excerpt": "false", + "dynamic": false, + "framework_default": false + } + ], + "lifecycle_paths": [ + { + "id": "lifecycle_path_9c0d9318b54ed0b7", + "artifact_ids": [ + "artifact_3be9a9359fa26462" + ], + "stages": [ + { + "stage": "validate", + "evidence_ids": [ + "evidence_19b19b3ff8c58764", + "evidence_2d5339153a64426a", + "evidence_32f7bf5d08e4949e", + "evidence_71052ce1d63e5977", + "evidence_7491e2691c2a91d9", + "evidence_756ae30fbb990830", + "evidence_8b4dac52bdc5a9a1", + "evidence_9faf5bd09db59388", + "evidence_cc285f22f612f773", + "evidence_d4b59bd0fc8ab376", + "evidence_e9efd84e48d00ea0" + ] + } + ], + "confidence": "low", + "dynamic": false, + "reviewer_question": "Which framework version and deployment settings determine lifecycle behavior for `access_jwt`?" + } + ], + "findings": [ + { + "id": "finding_640685938c485fae", + "category": "high_confidence_misconfiguration", + "severity": "high", + "artifact_ids": [ + "artifact_3be9a9359fa26462" + ], + "evidence_ids": [ + "evidence_19b19b3ff8c58764", + "evidence_2d5339153a64426a" + ], + "title": "JWT `access_jwt` validation accepts the `none` algorithm", + "description": "JWT verification evidence includes an explicit `none` algorithm allow-list entry.", + "suggested_fix": "Remove `none` from accepted algorithms and pin the expected signing algorithm.", + "reviewer_question": "Is any validation path intentionally accepting unsigned JWTs?" + }, + { + "id": "finding_e804d2976aba0c7d", + "category": "framework_default_assumed", + "severity": "low", + "artifact_ids": [ + "artifact_3be9a9359fa26462" + ], + "evidence_ids": [ + "evidence_756ae30fbb990830" + ], + "title": "JWT `access_jwt` expiry enforcement relies on library defaults", + "description": "JWT validation appears to rely on the library default for expiration enforcement.", + "suggested_fix": "Make expiration enforcement explicit or document the library version and default.", + "reviewer_question": "Which JWT library version and settings determine expiration enforcement here?" + } + ] +} \ No newline at end of file diff --git a/tests/integration/snapshots/generic-python-jwt-and-reset.json b/tests/integration/snapshots/generic-python-jwt-and-reset.json new file mode 100644 index 0000000..60439e5 --- /dev/null +++ b/tests/integration/snapshots/generic-python-jwt-and-reset.json @@ -0,0 +1,2227 @@ +{ + "schema_version": "0.5.0", + "summary": { + "files_discovered": 2, + "files_scanned": 2, + "files_skipped": 0, + "diagnostics": [], + "worker_panic_count": 0 + }, + "files": [ + { + "path": "auth.py", + "language": "python", + "artifacts": [ + { + "id": "artifact_a87fee6937c3c5c7", + "artifact_type": "password_reset_token", + "display_name": "password_reset_token", + "locations": [ + { + "path": "auth.py", + "line": 1, + "column": 1 + }, + { + "path": "auth.py", + "line": 50, + "column": 1 + }, + { + "path": "auth.py", + "line": 56, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_c5a43c05b6279e38" + ], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_80683a2cfc20663b" + ], + "expire": [ + "evidence_e9c83016ca543396" + ], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [] + }, + { + "id": "artifact_5ab48324a39c720a", + "artifact_type": "access_jwt", + "display_name": "access_jwt", + "locations": [ + { + "path": "auth.py", + "line": 13, + "column": 12 + }, + { + "path": "auth.py", + "line": 32, + "column": 12 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_0db6dad318cae9a9", + "evidence_1223d5c32d123d00", + "evidence_34d607a0bb1d3d05", + "evidence_66b972e811a6b871", + "evidence_8bf72e77fdfd44ba", + "evidence_8e62ee09cb0654d8", + "evidence_9160c5207317c308", + "evidence_b530b3c4d437e08f", + "evidence_c48ce5a507402d19", + "evidence_d884fc6584947a6b", + "evidence_e8028b0c166ba4b6" + ], + "store": [], + "transmit": [], + "validate": [ + "evidence_0ab760ae7e24a689", + "evidence_170f3794e1490ccc", + "evidence_1b688919407d804f", + "evidence_1d56066840a264c2", + "evidence_2c7d70c35fd4b8ad", + "evidence_a1e1544ec64e0957", + "evidence_d206780c19923731", + "evidence_f02b22268faf0d3f", + "evidence_fc5c6aaa1812035c", + "evidence_fec3b86f55948e8d" + ], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_3c608d31e7bef137" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "pyjwt" + ], + "jwt_attributes": { + "operation": { + "state": "present", + "value": "issue, validate", + "evidence_ids": [ + "evidence_34d607a0bb1d3d05", + "evidence_f02b22268faf0d3f" + ], + "confidence": "high" + }, + "algorithm": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_9160c5207317c308", + "evidence_a1e1544ec64e0957" + ], + "confidence": "high" + }, + "key_reference": { + "state": "present", + "value": "JWT_SECRET", + "evidence_ids": [ + "evidence_0ab760ae7e24a689", + "evidence_1223d5c32d123d00" + ], + "confidence": "high" + }, + "issuer": { + "state": "present", + "value": "ISSUER", + "evidence_ids": [ + "evidence_8bf72e77fdfd44ba", + "evidence_fec3b86f55948e8d" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_1b688919407d804f", + "evidence_8e62ee09cb0654d8" + ], + "confidence": "high" + }, + "expiration": { + "state": "present", + "value": "now + timedelta(minutes=15)", + "evidence_ids": [ + "evidence_3c608d31e7bef137" + ], + "confidence": "high" + }, + "signature_verification": { + "state": "present", + "value": "verified", + "evidence_ids": [ + "evidence_fc5c6aaa1812035c" + ], + "confidence": "high" + }, + "expiry_enforcement": { + "state": "framework_default", + "value": "PyJWT decode default", + "evidence_ids": [ + "evidence_2c7d70c35fd4b8ad" + ], + "confidence": "low" + }, + "identity_claims": { + "subject": { + "state": "present", + "value": "user_id", + "evidence_ids": [ + "evidence_e8028b0c166ba4b6" + ], + "confidence": "high" + }, + "user_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "tenant_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "org_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "workspace_id": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_c48ce5a507402d19" + ], + "confidence": "high" + }, + "roles": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "scopes": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "groups": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_0db6dad318cae9a9" + ], + "confidence": "high" + }, + "email": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_d884fc6584947a6b" + ], + "confidence": "high" + }, + "email_verified": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_66b972e811a6b871" + ], + "confidence": "high" + }, + "auth_method": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "auth_class": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_b530b3c4d437e08f" + ], + "confidence": "high" + } + } + }, + "token_boundary_attributes": { + "issuer": { + "state": "present", + "value": "ISSUER", + "evidence_ids": [ + "evidence_8bf72e77fdfd44ba", + "evidence_fec3b86f55948e8d" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_1b688919407d804f", + "evidence_8e62ee09cb0654d8" + ], + "confidence": "high" + }, + "service": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_c48ce5a507402d19" + ], + "confidence": "high" + }, + "trust_boundary": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_1b688919407d804f", + "evidence_8e62ee09cb0654d8" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_a567090b3e97a4ac", + "artifact_type": "unknown_token", + "display_name": "unknown_token", + "locations": [ + { + "path": "auth.py", + "line": 32, + "column": 12 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [ + "evidence_9542572d86d58518" + ], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [ + "evidence_29a5362edc28c085", + "evidence_6d7d27e2299a1798" + ] + }, + "confidence": "high", + "framework_hints": [ + "python" + ], + "token_boundary_attributes": { + "issuer": { + "state": "present", + "value": "issuer", + "evidence_ids": [ + "evidence_6d7d27e2299a1798" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "audience", + "evidence_ids": [ + "evidence_29a5362edc28c085" + ], + "confidence": "high" + }, + "scope": { + "state": "present", + "evidence_ids": [ + "evidence_9542572d86d58518" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_0501ca124ed65f0c", + "artifact_type": "access_jwt", + "display_name": "legacy_access_jwt", + "locations": [ + { + "path": "auth.py", + "line": 42, + "column": 12 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [ + "evidence_0137a9a1c5e9adb6", + "evidence_2d742ec24e4a7048", + "evidence_3d20bc7d9c1ee3c2", + "evidence_40ada113ad4ede86", + "evidence_95a08416ba76dc4e", + "evidence_aa4c099b052917bb", + "evidence_b82ffcde06e94810", + "evidence_d9f3ca1a5b645e72", + "evidence_e199d2bbe1351c3b" + ], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "pyjwt" + ], + "jwt_attributes": { + "operation": { + "state": "present", + "value": "validate", + "evidence_ids": [ + "evidence_3d20bc7d9c1ee3c2" + ], + "confidence": "high" + }, + "algorithm": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_d9f3ca1a5b645e72" + ], + "confidence": "high" + }, + "key_reference": { + "state": "present", + "value": "JWT_SECRET", + "evidence_ids": [ + "evidence_0137a9a1c5e9adb6" + ], + "confidence": "high" + }, + "issuer": { + "state": "missing", + "evidence_ids": [ + "evidence_e199d2bbe1351c3b" + ], + "confidence": "high" + }, + "audience": { + "state": "missing", + "evidence_ids": [ + "evidence_95a08416ba76dc4e" + ], + "confidence": "high" + }, + "expiration": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "signature_verification": { + "state": "present", + "value": "verified", + "evidence_ids": [ + "evidence_aa4c099b052917bb" + ], + "confidence": "high" + }, + "expiry_enforcement": { + "state": "missing", + "value": "verify_exp: false", + "evidence_ids": [ + "evidence_40ada113ad4ede86" + ], + "confidence": "high" + } + }, + "token_boundary_attributes": { + "issuer": { + "state": "missing", + "evidence_ids": [ + "evidence_e199d2bbe1351c3b" + ], + "confidence": "high" + }, + "audience": { + "state": "missing", + "evidence_ids": [ + "evidence_95a08416ba76dc4e" + ], + "confidence": "high" + }, + "provider": { + "state": "missing", + "evidence_ids": [ + "evidence_e199d2bbe1351c3b" + ], + "confidence": "high" + } + } + } + ], + "evidence": [ + { + "id": "evidence_e9c83016ca543396", + "lifecycle_stage": "expire", + "location": { + "path": "auth.py", + "line": 1, + "column": 1 + }, + "detector_id": "reset.expire", + "confidence": "medium", + "excerpt": "password_reset_token expire evidence", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_34d607a0bb1d3d05", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 13, + "column": 12 + }, + "detector_id": "jwt.issue", + "confidence": "high", + "excerpt": "pyjwt.encode issue call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_e8028b0c166ba4b6", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 15, + "column": 20 + }, + "detector_id": "jwt.attribute.subject", + "confidence": "high", + "excerpt": "subject claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c48ce5a507402d19", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 16, + "column": 29 + }, + "detector_id": "jwt.attribute.workspace_id", + "confidence": "high", + "excerpt": "workspace ID claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_0db6dad318cae9a9", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 17, + "column": 23 + }, + "detector_id": "jwt.attribute.groups", + "confidence": "high", + "excerpt": "groups claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_d884fc6584947a6b", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 18, + "column": 22 + }, + "detector_id": "jwt.attribute.email", + "confidence": "high", + "excerpt": "email claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_66b972e811a6b871", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 19, + "column": 31 + }, + "detector_id": "jwt.attribute.email_verified", + "confidence": "high", + "excerpt": "email verified claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_b530b3c4d437e08f", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 20, + "column": 20 + }, + "detector_id": "jwt.attribute.auth_class", + "confidence": "high", + "excerpt": "auth class claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_8bf72e77fdfd44ba", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 21, + "column": 20 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "ISSUER", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_8e62ee09cb0654d8", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 22, + "column": 20 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "AUDIENCE", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_3c608d31e7bef137", + "lifecycle_stage": "expire", + "location": { + "path": "auth.py", + "line": 24, + "column": 20 + }, + "detector_id": "jwt.attribute.expiration", + "confidence": "high", + "excerpt": "now + timedelta(minutes=15)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1223d5c32d123d00", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 26, + "column": 9 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9160c5207317c308", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 27, + "column": 19 + }, + "detector_id": "jwt.attribute.algorithm", + "confidence": "high", + "excerpt": "\"HS256\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9542572d86d58518", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 32, + "column": 12 + }, + "detector_id": "bearer.scope", + "confidence": "high", + "excerpt": "jwt.decode(\n token,\n JWT_SECRET,\n algorithms=[\"[REDACTED]\"],\n issuer=ISSUER,\n audience=AUDIENCE,\n )", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2c7d70c35fd4b8ad", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 32, + "column": 12 + }, + "detector_id": "jwt.attribute.expiry_enforcement", + "confidence": "low", + "excerpt": "PyJWT decode enforces exp by library default", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_fc5c6aaa1812035c", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 32, + "column": 12 + }, + "detector_id": "jwt.attribute.signature_verification", + "confidence": "high", + "excerpt": "PyJWT decode verifies signatures when a key is supplied", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_f02b22268faf0d3f", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 32, + "column": 12 + }, + "detector_id": "jwt.validate", + "confidence": "high", + "excerpt": "pyjwt.decode validate call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_29a5362edc28c085", + "lifecycle_stage": "introspect", + "location": { + "path": "auth.py", + "line": 32, + "column": 12 + }, + "detector_id": "bearer.boundary.audience", + "confidence": "high", + "excerpt": "jwt.decode(\n token,\n JWT_SECRET,\n algorithms=[\"[REDACTED]\"],\n issuer=ISSUER,\n audience=AUDIENCE,\n )", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_6d7d27e2299a1798", + "lifecycle_stage": "introspect", + "location": { + "path": "auth.py", + "line": 32, + "column": 12 + }, + "detector_id": "bearer.boundary.issuer", + "confidence": "high", + "excerpt": "jwt.decode(\n token,\n JWT_SECRET,\n algorithms=[\"[REDACTED]\"],\n issuer=ISSUER,\n audience=AUDIENCE,\n )", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_0ab760ae7e24a689", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 34, + "column": 9 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_a1e1544ec64e0957", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 35, + "column": 20 + }, + "detector_id": "jwt.attribute.algorithm", + "confidence": "high", + "excerpt": "[\"HS256\"]", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_d206780c19923731", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 35, + "column": 20 + }, + "detector_id": "jwt.option.algorithms", + "confidence": "high", + "excerpt": "[\"HS256\"]", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_fec3b86f55948e8d", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 36, + "column": 16 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "ISSUER", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_170f3794e1490ccc", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 36, + "column": 16 + }, + "detector_id": "jwt.option.issuer", + "confidence": "high", + "excerpt": "JWT issuer option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1b688919407d804f", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 37, + "column": 18 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "AUDIENCE", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1d56066840a264c2", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 37, + "column": 18 + }, + "detector_id": "jwt.option.audience", + "confidence": "high", + "excerpt": "JWT audience option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_95a08416ba76dc4e", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 42, + "column": 12 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "audience is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_40ada113ad4ede86", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 42, + "column": 12 + }, + "detector_id": "jwt.attribute.expiry_enforcement", + "confidence": "high", + "excerpt": "PyJWT expiry enforcement is disabled with verify_exp: false", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_e199d2bbe1351c3b", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 42, + "column": 12 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "issuer is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_aa4c099b052917bb", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 42, + "column": 12 + }, + "detector_id": "jwt.attribute.signature_verification", + "confidence": "high", + "excerpt": "PyJWT decode verifies signatures when a key is supplied", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_3d20bc7d9c1ee3c2", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 42, + "column": 12 + }, + "detector_id": "jwt.validate", + "confidence": "high", + "excerpt": "pyjwt.decode validate call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_0137a9a1c5e9adb6", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 44, + "column": 9 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_d9f3ca1a5b645e72", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 45, + "column": 20 + }, + "detector_id": "jwt.attribute.algorithm", + "confidence": "high", + "excerpt": "[\"HS256\"]", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_b82ffcde06e94810", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 45, + "column": 20 + }, + "detector_id": "jwt.option.algorithms", + "confidence": "high", + "excerpt": "[\"HS256\"]", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2d742ec24e4a7048", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 46, + "column": 32 + }, + "detector_id": "jwt.option.ignore_expiration", + "confidence": "high", + "excerpt": "False", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c5a43c05b6279e38", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 50, + "column": 1 + }, + "detector_id": "reset.issue", + "confidence": "medium", + "excerpt": "password_reset_token issue evidence", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_80683a2cfc20663b", + "lifecycle_stage": "revoke", + "location": { + "path": "auth.py", + "line": 56, + "column": 1 + }, + "detector_id": "reset.single_use", + "confidence": "medium", + "excerpt": "password_reset_token revoke evidence", + "dynamic": false, + "framework_default": false + } + ], + "diagnostics": [], + "skipped_reason": null + }, + { + "path": "expected.json", + "language": "json", + "artifacts": [], + "evidence": [], + "diagnostics": [], + "skipped_reason": null + } + ], + "artifacts": [ + { + "id": "artifact_a87fee6937c3c5c7", + "artifact_type": "password_reset_token", + "display_name": "password_reset_token", + "locations": [ + { + "path": "auth.py", + "line": 1, + "column": 1 + }, + { + "path": "auth.py", + "line": 50, + "column": 1 + }, + { + "path": "auth.py", + "line": 56, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_c5a43c05b6279e38" + ], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_80683a2cfc20663b" + ], + "expire": [ + "evidence_e9c83016ca543396" + ], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [] + }, + { + "id": "artifact_5ab48324a39c720a", + "artifact_type": "access_jwt", + "display_name": "access_jwt", + "locations": [ + { + "path": "auth.py", + "line": 13, + "column": 12 + }, + { + "path": "auth.py", + "line": 32, + "column": 12 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_0db6dad318cae9a9", + "evidence_1223d5c32d123d00", + "evidence_34d607a0bb1d3d05", + "evidence_66b972e811a6b871", + "evidence_8bf72e77fdfd44ba", + "evidence_8e62ee09cb0654d8", + "evidence_9160c5207317c308", + "evidence_b530b3c4d437e08f", + "evidence_c48ce5a507402d19", + "evidence_d884fc6584947a6b", + "evidence_e8028b0c166ba4b6" + ], + "store": [], + "transmit": [], + "validate": [ + "evidence_0ab760ae7e24a689", + "evidence_170f3794e1490ccc", + "evidence_1b688919407d804f", + "evidence_1d56066840a264c2", + "evidence_2c7d70c35fd4b8ad", + "evidence_a1e1544ec64e0957", + "evidence_d206780c19923731", + "evidence_f02b22268faf0d3f", + "evidence_fc5c6aaa1812035c", + "evidence_fec3b86f55948e8d" + ], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_3c608d31e7bef137" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "pyjwt" + ], + "jwt_attributes": { + "operation": { + "state": "present", + "value": "issue, validate", + "evidence_ids": [ + "evidence_34d607a0bb1d3d05", + "evidence_f02b22268faf0d3f" + ], + "confidence": "high" + }, + "algorithm": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_9160c5207317c308", + "evidence_a1e1544ec64e0957" + ], + "confidence": "high" + }, + "key_reference": { + "state": "present", + "value": "JWT_SECRET", + "evidence_ids": [ + "evidence_0ab760ae7e24a689", + "evidence_1223d5c32d123d00" + ], + "confidence": "high" + }, + "issuer": { + "state": "present", + "value": "ISSUER", + "evidence_ids": [ + "evidence_8bf72e77fdfd44ba", + "evidence_fec3b86f55948e8d" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_1b688919407d804f", + "evidence_8e62ee09cb0654d8" + ], + "confidence": "high" + }, + "expiration": { + "state": "present", + "value": "now + timedelta(minutes=15)", + "evidence_ids": [ + "evidence_3c608d31e7bef137" + ], + "confidence": "high" + }, + "signature_verification": { + "state": "present", + "value": "verified", + "evidence_ids": [ + "evidence_fc5c6aaa1812035c" + ], + "confidence": "high" + }, + "expiry_enforcement": { + "state": "framework_default", + "value": "PyJWT decode default", + "evidence_ids": [ + "evidence_2c7d70c35fd4b8ad" + ], + "confidence": "low" + }, + "identity_claims": { + "subject": { + "state": "present", + "value": "user_id", + "evidence_ids": [ + "evidence_e8028b0c166ba4b6" + ], + "confidence": "high" + }, + "user_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "tenant_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "org_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "workspace_id": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_c48ce5a507402d19" + ], + "confidence": "high" + }, + "roles": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "scopes": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "groups": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_0db6dad318cae9a9" + ], + "confidence": "high" + }, + "email": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_d884fc6584947a6b" + ], + "confidence": "high" + }, + "email_verified": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_66b972e811a6b871" + ], + "confidence": "high" + }, + "auth_method": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "auth_class": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_b530b3c4d437e08f" + ], + "confidence": "high" + } + } + }, + "token_boundary_attributes": { + "issuer": { + "state": "present", + "value": "ISSUER", + "evidence_ids": [ + "evidence_8bf72e77fdfd44ba", + "evidence_fec3b86f55948e8d" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_1b688919407d804f", + "evidence_8e62ee09cb0654d8" + ], + "confidence": "high" + }, + "service": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_c48ce5a507402d19" + ], + "confidence": "high" + }, + "trust_boundary": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_1b688919407d804f", + "evidence_8e62ee09cb0654d8" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_a567090b3e97a4ac", + "artifact_type": "unknown_token", + "display_name": "unknown_token", + "locations": [ + { + "path": "auth.py", + "line": 32, + "column": 12 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [ + "evidence_9542572d86d58518" + ], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [ + "evidence_29a5362edc28c085", + "evidence_6d7d27e2299a1798" + ] + }, + "confidence": "high", + "framework_hints": [ + "python" + ], + "token_boundary_attributes": { + "issuer": { + "state": "present", + "value": "issuer", + "evidence_ids": [ + "evidence_6d7d27e2299a1798" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "audience", + "evidence_ids": [ + "evidence_29a5362edc28c085" + ], + "confidence": "high" + }, + "scope": { + "state": "present", + "evidence_ids": [ + "evidence_9542572d86d58518" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_0501ca124ed65f0c", + "artifact_type": "access_jwt", + "display_name": "legacy_access_jwt", + "locations": [ + { + "path": "auth.py", + "line": 42, + "column": 12 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [ + "evidence_0137a9a1c5e9adb6", + "evidence_2d742ec24e4a7048", + "evidence_3d20bc7d9c1ee3c2", + "evidence_40ada113ad4ede86", + "evidence_95a08416ba76dc4e", + "evidence_aa4c099b052917bb", + "evidence_b82ffcde06e94810", + "evidence_d9f3ca1a5b645e72", + "evidence_e199d2bbe1351c3b" + ], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "pyjwt" + ], + "jwt_attributes": { + "operation": { + "state": "present", + "value": "validate", + "evidence_ids": [ + "evidence_3d20bc7d9c1ee3c2" + ], + "confidence": "high" + }, + "algorithm": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_d9f3ca1a5b645e72" + ], + "confidence": "high" + }, + "key_reference": { + "state": "present", + "value": "JWT_SECRET", + "evidence_ids": [ + "evidence_0137a9a1c5e9adb6" + ], + "confidence": "high" + }, + "issuer": { + "state": "missing", + "evidence_ids": [ + "evidence_e199d2bbe1351c3b" + ], + "confidence": "high" + }, + "audience": { + "state": "missing", + "evidence_ids": [ + "evidence_95a08416ba76dc4e" + ], + "confidence": "high" + }, + "expiration": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "signature_verification": { + "state": "present", + "value": "verified", + "evidence_ids": [ + "evidence_aa4c099b052917bb" + ], + "confidence": "high" + }, + "expiry_enforcement": { + "state": "missing", + "value": "verify_exp: false", + "evidence_ids": [ + "evidence_40ada113ad4ede86" + ], + "confidence": "high" + } + }, + "token_boundary_attributes": { + "issuer": { + "state": "missing", + "evidence_ids": [ + "evidence_e199d2bbe1351c3b" + ], + "confidence": "high" + }, + "audience": { + "state": "missing", + "evidence_ids": [ + "evidence_95a08416ba76dc4e" + ], + "confidence": "high" + }, + "provider": { + "state": "missing", + "evidence_ids": [ + "evidence_e199d2bbe1351c3b" + ], + "confidence": "high" + } + } + } + ], + "evidence": [ + { + "id": "evidence_e9c83016ca543396", + "lifecycle_stage": "expire", + "location": { + "path": "auth.py", + "line": 1, + "column": 1 + }, + "detector_id": "reset.expire", + "confidence": "medium", + "excerpt": "password_reset_token expire evidence", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_34d607a0bb1d3d05", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 13, + "column": 12 + }, + "detector_id": "jwt.issue", + "confidence": "high", + "excerpt": "pyjwt.encode issue call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_e8028b0c166ba4b6", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 15, + "column": 20 + }, + "detector_id": "jwt.attribute.subject", + "confidence": "high", + "excerpt": "subject claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c48ce5a507402d19", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 16, + "column": 29 + }, + "detector_id": "jwt.attribute.workspace_id", + "confidence": "high", + "excerpt": "workspace ID claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_0db6dad318cae9a9", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 17, + "column": 23 + }, + "detector_id": "jwt.attribute.groups", + "confidence": "high", + "excerpt": "groups claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_d884fc6584947a6b", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 18, + "column": 22 + }, + "detector_id": "jwt.attribute.email", + "confidence": "high", + "excerpt": "email claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_66b972e811a6b871", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 19, + "column": 31 + }, + "detector_id": "jwt.attribute.email_verified", + "confidence": "high", + "excerpt": "email verified claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_b530b3c4d437e08f", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 20, + "column": 20 + }, + "detector_id": "jwt.attribute.auth_class", + "confidence": "high", + "excerpt": "auth class claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_8bf72e77fdfd44ba", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 21, + "column": 20 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "ISSUER", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_8e62ee09cb0654d8", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 22, + "column": 20 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "AUDIENCE", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_3c608d31e7bef137", + "lifecycle_stage": "expire", + "location": { + "path": "auth.py", + "line": 24, + "column": 20 + }, + "detector_id": "jwt.attribute.expiration", + "confidence": "high", + "excerpt": "now + timedelta(minutes=15)", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1223d5c32d123d00", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 26, + "column": 9 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9160c5207317c308", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 27, + "column": 19 + }, + "detector_id": "jwt.attribute.algorithm", + "confidence": "high", + "excerpt": "\"HS256\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9542572d86d58518", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 32, + "column": 12 + }, + "detector_id": "bearer.scope", + "confidence": "high", + "excerpt": "jwt.decode(\n token,\n JWT_SECRET,\n algorithms=[\"[REDACTED]\"],\n issuer=ISSUER,\n audience=AUDIENCE,\n )", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2c7d70c35fd4b8ad", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 32, + "column": 12 + }, + "detector_id": "jwt.attribute.expiry_enforcement", + "confidence": "low", + "excerpt": "PyJWT decode enforces exp by library default", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_fc5c6aaa1812035c", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 32, + "column": 12 + }, + "detector_id": "jwt.attribute.signature_verification", + "confidence": "high", + "excerpt": "PyJWT decode verifies signatures when a key is supplied", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_f02b22268faf0d3f", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 32, + "column": 12 + }, + "detector_id": "jwt.validate", + "confidence": "high", + "excerpt": "pyjwt.decode validate call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_29a5362edc28c085", + "lifecycle_stage": "introspect", + "location": { + "path": "auth.py", + "line": 32, + "column": 12 + }, + "detector_id": "bearer.boundary.audience", + "confidence": "high", + "excerpt": "jwt.decode(\n token,\n JWT_SECRET,\n algorithms=[\"[REDACTED]\"],\n issuer=ISSUER,\n audience=AUDIENCE,\n )", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_6d7d27e2299a1798", + "lifecycle_stage": "introspect", + "location": { + "path": "auth.py", + "line": 32, + "column": 12 + }, + "detector_id": "bearer.boundary.issuer", + "confidence": "high", + "excerpt": "jwt.decode(\n token,\n JWT_SECRET,\n algorithms=[\"[REDACTED]\"],\n issuer=ISSUER,\n audience=AUDIENCE,\n )", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_0ab760ae7e24a689", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 34, + "column": 9 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_a1e1544ec64e0957", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 35, + "column": 20 + }, + "detector_id": "jwt.attribute.algorithm", + "confidence": "high", + "excerpt": "[\"HS256\"]", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_d206780c19923731", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 35, + "column": 20 + }, + "detector_id": "jwt.option.algorithms", + "confidence": "high", + "excerpt": "[\"HS256\"]", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_fec3b86f55948e8d", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 36, + "column": 16 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "ISSUER", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_170f3794e1490ccc", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 36, + "column": 16 + }, + "detector_id": "jwt.option.issuer", + "confidence": "high", + "excerpt": "JWT issuer option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1b688919407d804f", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 37, + "column": 18 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "AUDIENCE", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1d56066840a264c2", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 37, + "column": 18 + }, + "detector_id": "jwt.option.audience", + "confidence": "high", + "excerpt": "JWT audience option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_95a08416ba76dc4e", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 42, + "column": 12 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "audience is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_40ada113ad4ede86", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 42, + "column": 12 + }, + "detector_id": "jwt.attribute.expiry_enforcement", + "confidence": "high", + "excerpt": "PyJWT expiry enforcement is disabled with verify_exp: false", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_e199d2bbe1351c3b", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 42, + "column": 12 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "issuer is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_aa4c099b052917bb", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 42, + "column": 12 + }, + "detector_id": "jwt.attribute.signature_verification", + "confidence": "high", + "excerpt": "PyJWT decode verifies signatures when a key is supplied", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_3d20bc7d9c1ee3c2", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 42, + "column": 12 + }, + "detector_id": "jwt.validate", + "confidence": "high", + "excerpt": "pyjwt.decode validate call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_0137a9a1c5e9adb6", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 44, + "column": 9 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_d9f3ca1a5b645e72", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 45, + "column": 20 + }, + "detector_id": "jwt.attribute.algorithm", + "confidence": "high", + "excerpt": "[\"HS256\"]", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_b82ffcde06e94810", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 45, + "column": 20 + }, + "detector_id": "jwt.option.algorithms", + "confidence": "high", + "excerpt": "[\"HS256\"]", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2d742ec24e4a7048", + "lifecycle_stage": "validate", + "location": { + "path": "auth.py", + "line": 46, + "column": 32 + }, + "detector_id": "jwt.option.ignore_expiration", + "confidence": "high", + "excerpt": "False", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c5a43c05b6279e38", + "lifecycle_stage": "issue", + "location": { + "path": "auth.py", + "line": 50, + "column": 1 + }, + "detector_id": "reset.issue", + "confidence": "medium", + "excerpt": "password_reset_token issue evidence", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_80683a2cfc20663b", + "lifecycle_stage": "revoke", + "location": { + "path": "auth.py", + "line": 56, + "column": 1 + }, + "detector_id": "reset.single_use", + "confidence": "medium", + "excerpt": "password_reset_token revoke evidence", + "dynamic": false, + "framework_default": false + } + ], + "lifecycle_paths": [ + { + "id": "lifecycle_path_982133d31f7e5221", + "artifact_ids": [ + "artifact_0501ca124ed65f0c" + ], + "stages": [ + { + "stage": "validate", + "evidence_ids": [ + "evidence_0137a9a1c5e9adb6", + "evidence_2d742ec24e4a7048", + "evidence_3d20bc7d9c1ee3c2", + "evidence_40ada113ad4ede86", + "evidence_95a08416ba76dc4e", + "evidence_aa4c099b052917bb", + "evidence_b82ffcde06e94810", + "evidence_d9f3ca1a5b645e72", + "evidence_e199d2bbe1351c3b" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_ba5f204467cb2a68", + "artifact_ids": [ + "artifact_5ab48324a39c720a" + ], + "stages": [ + { + "stage": "issue", + "evidence_ids": [ + "evidence_0db6dad318cae9a9", + "evidence_1223d5c32d123d00", + "evidence_34d607a0bb1d3d05", + "evidence_66b972e811a6b871", + "evidence_8bf72e77fdfd44ba", + "evidence_8e62ee09cb0654d8", + "evidence_9160c5207317c308", + "evidence_b530b3c4d437e08f", + "evidence_c48ce5a507402d19", + "evidence_d884fc6584947a6b", + "evidence_e8028b0c166ba4b6" + ] + }, + { + "stage": "validate", + "evidence_ids": [ + "evidence_0ab760ae7e24a689", + "evidence_170f3794e1490ccc", + "evidence_1b688919407d804f", + "evidence_1d56066840a264c2", + "evidence_2c7d70c35fd4b8ad", + "evidence_a1e1544ec64e0957", + "evidence_d206780c19923731", + "evidence_f02b22268faf0d3f", + "evidence_fc5c6aaa1812035c", + "evidence_fec3b86f55948e8d" + ] + }, + { + "stage": "expire", + "evidence_ids": [ + "evidence_3c608d31e7bef137" + ] + } + ], + "confidence": "low", + "dynamic": false, + "reviewer_question": "Which framework version and deployment settings determine lifecycle behavior for `access_jwt`?" + }, + { + "id": "lifecycle_path_a44c356c99aa4aab", + "artifact_ids": [ + "artifact_a567090b3e97a4ac" + ], + "stages": [ + { + "stage": "validate", + "evidence_ids": [ + "evidence_9542572d86d58518" + ] + }, + { + "stage": "introspect", + "evidence_ids": [ + "evidence_29a5362edc28c085", + "evidence_6d7d27e2299a1798" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_c9f1e8d4e5b9d941", + "artifact_ids": [ + "artifact_a87fee6937c3c5c7" + ], + "stages": [ + { + "stage": "issue", + "evidence_ids": [ + "evidence_c5a43c05b6279e38" + ] + }, + { + "stage": "revoke", + "evidence_ids": [ + "evidence_80683a2cfc20663b" + ] + }, + { + "stage": "expire", + "evidence_ids": [ + "evidence_e9c83016ca543396" + ] + } + ], + "confidence": "medium", + "dynamic": false, + "reviewer_question": null + } + ], + "findings": [ + { + "id": "finding_660d8ee7895a862d", + "category": "high_confidence_misconfiguration", + "severity": "high", + "artifact_ids": [ + "artifact_0501ca124ed65f0c" + ], + "evidence_ids": [ + "evidence_40ada113ad4ede86" + ], + "title": "JWT `legacy_access_jwt` expiry enforcement is disabled or absent", + "description": "Evidence shows this JWT validation path does not enforce expiration.", + "suggested_fix": "Require expiration enforcement when validating JWTs.", + "reviewer_question": "Can expired tokens be accepted on this path?" + }, + { + "id": "finding_e3f631efe8ee5d51", + "category": "missing_validation_evidence", + "severity": "medium", + "artifact_ids": [ + "artifact_0501ca124ed65f0c" + ], + "evidence_ids": [ + "evidence_95a08416ba76dc4e" + ], + "title": "JWT `legacy_access_jwt` verification has no audience evidence", + "description": "JWT verification evidence does not include an expected audience check.", + "suggested_fix": "Require an expected audience when verifying JWTs.", + "reviewer_question": "Should this service reject tokens with an unexpected audience?" + }, + { + "id": "finding_dbaf38232e9298df", + "category": "missing_validation_evidence", + "severity": "medium", + "artifact_ids": [ + "artifact_0501ca124ed65f0c" + ], + "evidence_ids": [ + "evidence_e199d2bbe1351c3b" + ], + "title": "JWT `legacy_access_jwt` verification has no issuer evidence", + "description": "JWT verification evidence does not include an expected issuer check.", + "suggested_fix": "Require an expected issuer when verifying JWTs.", + "reviewer_question": "Should this service reject tokens with an unexpected issuer?" + }, + { + "id": "finding_f4a7e65729dbbfbd", + "category": "missing_validation_evidence", + "severity": "low", + "artifact_ids": [ + "artifact_0501ca124ed65f0c" + ], + "evidence_ids": [ + "evidence_3d20bc7d9c1ee3c2", + "evidence_d9f3ca1a5b645e72", + "evidence_0137a9a1c5e9adb6", + "evidence_e199d2bbe1351c3b", + "evidence_95a08416ba76dc4e", + "evidence_aa4c099b052917bb", + "evidence_40ada113ad4ede86", + "evidence_b82ffcde06e94810", + "evidence_2d742ec24e4a7048" + ], + "title": "JWT `legacy_access_jwt` verification has no not-before validation evidence", + "description": "JWT verification evidence does not show `nbf` / not-before claim enforcement.", + "suggested_fix": "Require or enforce `nbf` validation where issuer policy uses not-before claims.", + "reviewer_question": "Do issued tokens for this path use `nbf`, and is it enforced in production?" + }, + { + "id": "finding_5a562da4e436373c", + "category": "missing_validation_evidence", + "severity": "low", + "artifact_ids": [ + "artifact_5ab48324a39c720a" + ], + "evidence_ids": [ + "evidence_f02b22268faf0d3f", + "evidence_a1e1544ec64e0957", + "evidence_0ab760ae7e24a689", + "evidence_fec3b86f55948e8d", + "evidence_1b688919407d804f", + "evidence_fc5c6aaa1812035c", + "evidence_2c7d70c35fd4b8ad", + "evidence_d206780c19923731", + "evidence_1d56066840a264c2", + "evidence_170f3794e1490ccc" + ], + "title": "JWT `access_jwt` verification has no not-before validation evidence", + "description": "JWT verification evidence does not show `nbf` / not-before claim enforcement.", + "suggested_fix": "Require or enforce `nbf` validation where issuer policy uses not-before claims.", + "reviewer_question": "Do issued tokens for this path use `nbf`, and is it enforced in production?" + }, + { + "id": "finding_3279b41414008638", + "category": "framework_default_assumed", + "severity": "low", + "artifact_ids": [ + "artifact_5ab48324a39c720a" + ], + "evidence_ids": [ + "evidence_2c7d70c35fd4b8ad" + ], + "title": "JWT `access_jwt` expiry enforcement relies on library defaults", + "description": "JWT validation appears to rely on the library default for expiration enforcement.", + "suggested_fix": "Make expiration enforcement explicit or document the library version and default.", + "reviewer_question": "Which JWT library version and settings determine expiration enforcement here?" + } + ] +} \ No newline at end of file diff --git a/tests/integration/snapshots/generic-ts-jwt-validation.json b/tests/integration/snapshots/generic-ts-jwt-validation.json new file mode 100644 index 0000000..9893b9e --- /dev/null +++ b/tests/integration/snapshots/generic-ts-jwt-validation.json @@ -0,0 +1,1874 @@ +{ + "schema_version": "0.5.0", + "summary": { + "files_discovered": 2, + "files_scanned": 2, + "files_skipped": 0, + "diagnostics": [], + "worker_panic_count": 0 + }, + "files": [ + { + "path": "auth.ts", + "language": "type_script", + "artifacts": [ + { + "id": "artifact_59b937e65bbea018", + "artifact_type": "access_jwt", + "display_name": "access_jwt", + "locations": [ + { + "path": "auth.ts", + "line": 17, + "column": 10 + }, + { + "path": "auth.ts", + "line": 29, + "column": 10 + }, + { + "path": "auth.ts", + "line": 40, + "column": 10 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_08cd1591819587b4", + "evidence_0dcaee30a4dba499", + "evidence_0f5a9f0d5b2b4dc0", + "evidence_48517d1eba13bd74", + "evidence_5fe4ec412b8292a8", + "evidence_7e70705f466b78bd", + "evidence_7f684ad11256f8c0", + "evidence_a16bbcfe379030e8", + "evidence_e2260e9f2965c63f", + "evidence_f014933fd3dda9ba", + "evidence_fc13667fe57d80c7" + ], + "store": [], + "transmit": [], + "validate": [ + "evidence_142607e448a7c55a", + "evidence_2c55d5235636e020", + "evidence_2f1f7bb096bf5b6b", + "evidence_5794655114427a01", + "evidence_5ff7f5334ab5b971", + "evidence_c8db640fa216830b", + "evidence_eb240a6458b51f7d", + "evidence_ef5a75f64cb806d4", + "evidence_f60f8290316ad4bb", + "evidence_ffb17cdf5e5e8b20" + ], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_7f761cb194aa88ef" + ], + "introspect": [ + "evidence_1c994198cf6dc30c" + ] + }, + "confidence": "high", + "framework_hints": [ + "jsonwebtoken" + ], + "jwt_attributes": { + "operation": { + "state": "present", + "value": "decode_without_verify, issue, validate", + "evidence_ids": [ + "evidence_1c994198cf6dc30c", + "evidence_eb240a6458b51f7d", + "evidence_fc13667fe57d80c7" + ], + "confidence": "high" + }, + "algorithm": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "key_reference": { + "state": "present", + "value": "JWT_SECRET", + "evidence_ids": [ + "evidence_7f684ad11256f8c0", + "evidence_ffb17cdf5e5e8b20" + ], + "confidence": "high" + }, + "issuer": { + "state": "present", + "value": "ISSUER", + "evidence_ids": [ + "evidence_08cd1591819587b4", + "evidence_2c55d5235636e020" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_142607e448a7c55a", + "evidence_5fe4ec412b8292a8" + ], + "confidence": "high" + }, + "expiration": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_7f761cb194aa88ef" + ], + "confidence": "high" + }, + "signature_verification": { + "state": "missing", + "value": "decode_without_verify, verified", + "evidence_ids": [ + "evidence_ef5a75f64cb806d4", + "evidence_f60f8290316ad4bb" + ], + "confidence": "high" + }, + "expiry_enforcement": { + "state": "missing", + "value": "decode_without_verify, jsonwebtoken.verify default", + "evidence_ids": [ + "evidence_5794655114427a01", + "evidence_c8db640fa216830b" + ], + "confidence": "high" + }, + "identity_claims": { + "subject": { + "state": "present", + "value": "userId", + "evidence_ids": [ + "evidence_0f5a9f0d5b2b4dc0" + ], + "confidence": "high" + }, + "user_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "tenant_id": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_a16bbcfe379030e8" + ], + "confidence": "high" + }, + "org_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "workspace_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "roles": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_48517d1eba13bd74" + ], + "confidence": "high" + }, + "scopes": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_0dcaee30a4dba499" + ], + "confidence": "high" + }, + "groups": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "email": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_e2260e9f2965c63f" + ], + "confidence": "high" + }, + "email_verified": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_f014933fd3dda9ba" + ], + "confidence": "high" + }, + "auth_method": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_7e70705f466b78bd" + ], + "confidence": "high" + }, + "auth_class": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + } + } + }, + "token_boundary_attributes": { + "issuer": { + "state": "present", + "value": "ISSUER", + "evidence_ids": [ + "evidence_08cd1591819587b4", + "evidence_2c55d5235636e020" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_142607e448a7c55a", + "evidence_5fe4ec412b8292a8" + ], + "confidence": "high" + }, + "tenant": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_a16bbcfe379030e8" + ], + "confidence": "high" + }, + "scope": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_0dcaee30a4dba499" + ], + "confidence": "high" + }, + "trust_boundary": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_142607e448a7c55a", + "evidence_5fe4ec412b8292a8" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_e6e873017238673a", + "artifact_type": "access_jwt", + "display_name": "legacy_access_jwt", + "locations": [ + { + "path": "auth.ts", + "line": 36, + "column": 10 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [ + "evidence_34b40fb747ca2a6e", + "evidence_3544c272b486ee48", + "evidence_49fd2c2d6edf3ffa", + "evidence_5b152f496cd87501", + "evidence_7d0978c8a0dc497a", + "evidence_8f9a6b8553ad8d39", + "evidence_c262b6b3fe4e626d" + ], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "jsonwebtoken" + ], + "jwt_attributes": { + "operation": { + "state": "present", + "value": "validate", + "evidence_ids": [ + "evidence_c262b6b3fe4e626d" + ], + "confidence": "high" + }, + "algorithm": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "key_reference": { + "state": "present", + "value": "JWT_SECRET", + "evidence_ids": [ + "evidence_34b40fb747ca2a6e" + ], + "confidence": "high" + }, + "issuer": { + "state": "missing", + "evidence_ids": [ + "evidence_7d0978c8a0dc497a" + ], + "confidence": "high" + }, + "audience": { + "state": "missing", + "evidence_ids": [ + "evidence_8f9a6b8553ad8d39" + ], + "confidence": "high" + }, + "expiration": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "signature_verification": { + "state": "present", + "value": "verified", + "evidence_ids": [ + "evidence_49fd2c2d6edf3ffa" + ], + "confidence": "high" + }, + "expiry_enforcement": { + "state": "missing", + "value": "ignoreExpiration: true", + "evidence_ids": [ + "evidence_5b152f496cd87501" + ], + "confidence": "high" + } + }, + "token_boundary_attributes": { + "issuer": { + "state": "missing", + "evidence_ids": [ + "evidence_7d0978c8a0dc497a" + ], + "confidence": "high" + }, + "audience": { + "state": "missing", + "evidence_ids": [ + "evidence_8f9a6b8553ad8d39" + ], + "confidence": "high" + }, + "provider": { + "state": "missing", + "evidence_ids": [ + "evidence_7d0978c8a0dc497a" + ], + "confidence": "high" + } + } + } + ], + "evidence": [ + { + "id": "evidence_0f5a9f0d5b2b4dc0", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 9, + "column": 10 + }, + "detector_id": "jwt.attribute.subject", + "confidence": "high", + "excerpt": "subject claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_a16bbcfe379030e8", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 10, + "column": 16 + }, + "detector_id": "jwt.attribute.tenant_id", + "confidence": "high", + "excerpt": "tenant ID claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_48517d1eba13bd74", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 11, + "column": 12 + }, + "detector_id": "jwt.attribute.roles", + "confidence": "high", + "excerpt": "roles claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_0dcaee30a4dba499", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 12, + "column": 12 + }, + "detector_id": "jwt.attribute.scopes", + "confidence": "high", + "excerpt": "scopes claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_e2260e9f2965c63f", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 13, + "column": 12 + }, + "detector_id": "jwt.attribute.email", + "confidence": "high", + "excerpt": "email claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_f014933fd3dda9ba", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 14, + "column": 20 + }, + "detector_id": "jwt.attribute.email_verified", + "confidence": "high", + "excerpt": "email verified claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_7e70705f466b78bd", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 15, + "column": 10 + }, + "detector_id": "jwt.attribute.auth_method", + "confidence": "high", + "excerpt": "auth method claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_fc13667fe57d80c7", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 17, + "column": 10 + }, + "detector_id": "jwt.issue", + "confidence": "high", + "excerpt": "jsonwebtoken.sign issue call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_7f684ad11256f8c0", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 19, + "column": 5 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_08cd1591819587b4", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 21, + "column": 15 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "ISSUER", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_5fe4ec412b8292a8", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 22, + "column": 17 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "AUDIENCE", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_7f761cb194aa88ef", + "lifecycle_stage": "expire", + "location": { + "path": "auth.ts", + "line": 23, + "column": 18 + }, + "detector_id": "jwt.attribute.expiration", + "confidence": "high", + "excerpt": "\"15m\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c8db640fa216830b", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 29, + "column": 10 + }, + "detector_id": "jwt.attribute.expiry_enforcement", + "confidence": "low", + "excerpt": "jsonwebtoken.verify enforces exp by library default", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_ef5a75f64cb806d4", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 29, + "column": 10 + }, + "detector_id": "jwt.attribute.signature_verification", + "confidence": "high", + "excerpt": "jsonwebtoken.verify verifies JWT signatures", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_eb240a6458b51f7d", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 29, + "column": 10 + }, + "detector_id": "jwt.validate", + "confidence": "high", + "excerpt": "jsonwebtoken.verify validate call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_ffb17cdf5e5e8b20", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 29, + "column": 28 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2c55d5235636e020", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 30, + "column": 13 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "ISSUER", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2f1f7bb096bf5b6b", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 30, + "column": 13 + }, + "detector_id": "jwt.option.issuer", + "confidence": "high", + "excerpt": "JWT issuer option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_142607e448a7c55a", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 31, + "column": 15 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "AUDIENCE", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_5ff7f5334ab5b971", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 31, + "column": 15 + }, + "detector_id": "jwt.option.audience", + "confidence": "high", + "excerpt": "JWT audience option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_8f9a6b8553ad8d39", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 36, + "column": 10 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "audience is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_5b152f496cd87501", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 36, + "column": 10 + }, + "detector_id": "jwt.attribute.expiry_enforcement", + "confidence": "high", + "excerpt": "JWT expiry enforcement is disabled with ignoreExpiration: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_7d0978c8a0dc497a", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 36, + "column": 10 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "issuer is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_49fd2c2d6edf3ffa", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 36, + "column": 10 + }, + "detector_id": "jwt.attribute.signature_verification", + "confidence": "high", + "excerpt": "jsonwebtoken.verify verifies JWT signatures", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c262b6b3fe4e626d", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 36, + "column": 10 + }, + "detector_id": "jwt.validate", + "confidence": "high", + "excerpt": "jsonwebtoken.verify validate call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_34b40fb747ca2a6e", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 36, + "column": 28 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_3544c272b486ee48", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 36, + "column": 60 + }, + "detector_id": "jwt.option.ignore_expiration", + "confidence": "high", + "excerpt": "true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_5794655114427a01", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 40, + "column": 10 + }, + "detector_id": "jwt.attribute.expiry_enforcement", + "confidence": "high", + "excerpt": "jsonwebtoken.decode does not enforce JWT expiration", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_f60f8290316ad4bb", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 40, + "column": 10 + }, + "detector_id": "jwt.attribute.signature_verification", + "confidence": "high", + "excerpt": "jsonwebtoken.decode decodes JWTs without signature verification", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1c994198cf6dc30c", + "lifecycle_stage": "introspect", + "location": { + "path": "auth.ts", + "line": 40, + "column": 10 + }, + "detector_id": "jwt.decode_without_verify", + "confidence": "high", + "excerpt": "jsonwebtoken.decode decode_without_verify call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + } + ], + "diagnostics": [], + "skipped_reason": null + }, + { + "path": "expected.json", + "language": "json", + "artifacts": [], + "evidence": [], + "diagnostics": [], + "skipped_reason": null + } + ], + "artifacts": [ + { + "id": "artifact_59b937e65bbea018", + "artifact_type": "access_jwt", + "display_name": "access_jwt", + "locations": [ + { + "path": "auth.ts", + "line": 17, + "column": 10 + }, + { + "path": "auth.ts", + "line": 29, + "column": 10 + }, + { + "path": "auth.ts", + "line": 40, + "column": 10 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_08cd1591819587b4", + "evidence_0dcaee30a4dba499", + "evidence_0f5a9f0d5b2b4dc0", + "evidence_48517d1eba13bd74", + "evidence_5fe4ec412b8292a8", + "evidence_7e70705f466b78bd", + "evidence_7f684ad11256f8c0", + "evidence_a16bbcfe379030e8", + "evidence_e2260e9f2965c63f", + "evidence_f014933fd3dda9ba", + "evidence_fc13667fe57d80c7" + ], + "store": [], + "transmit": [], + "validate": [ + "evidence_142607e448a7c55a", + "evidence_2c55d5235636e020", + "evidence_2f1f7bb096bf5b6b", + "evidence_5794655114427a01", + "evidence_5ff7f5334ab5b971", + "evidence_c8db640fa216830b", + "evidence_eb240a6458b51f7d", + "evidence_ef5a75f64cb806d4", + "evidence_f60f8290316ad4bb", + "evidence_ffb17cdf5e5e8b20" + ], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_7f761cb194aa88ef" + ], + "introspect": [ + "evidence_1c994198cf6dc30c" + ] + }, + "confidence": "high", + "framework_hints": [ + "jsonwebtoken" + ], + "jwt_attributes": { + "operation": { + "state": "present", + "value": "decode_without_verify, issue, validate", + "evidence_ids": [ + "evidence_1c994198cf6dc30c", + "evidence_eb240a6458b51f7d", + "evidence_fc13667fe57d80c7" + ], + "confidence": "high" + }, + "algorithm": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "key_reference": { + "state": "present", + "value": "JWT_SECRET", + "evidence_ids": [ + "evidence_7f684ad11256f8c0", + "evidence_ffb17cdf5e5e8b20" + ], + "confidence": "high" + }, + "issuer": { + "state": "present", + "value": "ISSUER", + "evidence_ids": [ + "evidence_08cd1591819587b4", + "evidence_2c55d5235636e020" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_142607e448a7c55a", + "evidence_5fe4ec412b8292a8" + ], + "confidence": "high" + }, + "expiration": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_7f761cb194aa88ef" + ], + "confidence": "high" + }, + "signature_verification": { + "state": "missing", + "value": "decode_without_verify, verified", + "evidence_ids": [ + "evidence_ef5a75f64cb806d4", + "evidence_f60f8290316ad4bb" + ], + "confidence": "high" + }, + "expiry_enforcement": { + "state": "missing", + "value": "decode_without_verify, jsonwebtoken.verify default", + "evidence_ids": [ + "evidence_5794655114427a01", + "evidence_c8db640fa216830b" + ], + "confidence": "high" + }, + "identity_claims": { + "subject": { + "state": "present", + "value": "userId", + "evidence_ids": [ + "evidence_0f5a9f0d5b2b4dc0" + ], + "confidence": "high" + }, + "user_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "tenant_id": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_a16bbcfe379030e8" + ], + "confidence": "high" + }, + "org_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "workspace_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "roles": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_48517d1eba13bd74" + ], + "confidence": "high" + }, + "scopes": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_0dcaee30a4dba499" + ], + "confidence": "high" + }, + "groups": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "email": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_e2260e9f2965c63f" + ], + "confidence": "high" + }, + "email_verified": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_f014933fd3dda9ba" + ], + "confidence": "high" + }, + "auth_method": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_7e70705f466b78bd" + ], + "confidence": "high" + }, + "auth_class": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + } + } + }, + "token_boundary_attributes": { + "issuer": { + "state": "present", + "value": "ISSUER", + "evidence_ids": [ + "evidence_08cd1591819587b4", + "evidence_2c55d5235636e020" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_142607e448a7c55a", + "evidence_5fe4ec412b8292a8" + ], + "confidence": "high" + }, + "tenant": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_a16bbcfe379030e8" + ], + "confidence": "high" + }, + "scope": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_0dcaee30a4dba499" + ], + "confidence": "high" + }, + "trust_boundary": { + "state": "present", + "value": "AUDIENCE", + "evidence_ids": [ + "evidence_142607e448a7c55a", + "evidence_5fe4ec412b8292a8" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_e6e873017238673a", + "artifact_type": "access_jwt", + "display_name": "legacy_access_jwt", + "locations": [ + { + "path": "auth.ts", + "line": 36, + "column": 10 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [ + "evidence_34b40fb747ca2a6e", + "evidence_3544c272b486ee48", + "evidence_49fd2c2d6edf3ffa", + "evidence_5b152f496cd87501", + "evidence_7d0978c8a0dc497a", + "evidence_8f9a6b8553ad8d39", + "evidence_c262b6b3fe4e626d" + ], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "jsonwebtoken" + ], + "jwt_attributes": { + "operation": { + "state": "present", + "value": "validate", + "evidence_ids": [ + "evidence_c262b6b3fe4e626d" + ], + "confidence": "high" + }, + "algorithm": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "key_reference": { + "state": "present", + "value": "JWT_SECRET", + "evidence_ids": [ + "evidence_34b40fb747ca2a6e" + ], + "confidence": "high" + }, + "issuer": { + "state": "missing", + "evidence_ids": [ + "evidence_7d0978c8a0dc497a" + ], + "confidence": "high" + }, + "audience": { + "state": "missing", + "evidence_ids": [ + "evidence_8f9a6b8553ad8d39" + ], + "confidence": "high" + }, + "expiration": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "signature_verification": { + "state": "present", + "value": "verified", + "evidence_ids": [ + "evidence_49fd2c2d6edf3ffa" + ], + "confidence": "high" + }, + "expiry_enforcement": { + "state": "missing", + "value": "ignoreExpiration: true", + "evidence_ids": [ + "evidence_5b152f496cd87501" + ], + "confidence": "high" + } + }, + "token_boundary_attributes": { + "issuer": { + "state": "missing", + "evidence_ids": [ + "evidence_7d0978c8a0dc497a" + ], + "confidence": "high" + }, + "audience": { + "state": "missing", + "evidence_ids": [ + "evidence_8f9a6b8553ad8d39" + ], + "confidence": "high" + }, + "provider": { + "state": "missing", + "evidence_ids": [ + "evidence_7d0978c8a0dc497a" + ], + "confidence": "high" + } + } + } + ], + "evidence": [ + { + "id": "evidence_0f5a9f0d5b2b4dc0", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 9, + "column": 10 + }, + "detector_id": "jwt.attribute.subject", + "confidence": "high", + "excerpt": "subject claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_a16bbcfe379030e8", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 10, + "column": 16 + }, + "detector_id": "jwt.attribute.tenant_id", + "confidence": "high", + "excerpt": "tenant ID claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_48517d1eba13bd74", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 11, + "column": 12 + }, + "detector_id": "jwt.attribute.roles", + "confidence": "high", + "excerpt": "roles claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_0dcaee30a4dba499", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 12, + "column": 12 + }, + "detector_id": "jwt.attribute.scopes", + "confidence": "high", + "excerpt": "scopes claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_e2260e9f2965c63f", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 13, + "column": 12 + }, + "detector_id": "jwt.attribute.email", + "confidence": "high", + "excerpt": "email claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_f014933fd3dda9ba", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 14, + "column": 20 + }, + "detector_id": "jwt.attribute.email_verified", + "confidence": "high", + "excerpt": "email verified claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_7e70705f466b78bd", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 15, + "column": 10 + }, + "detector_id": "jwt.attribute.auth_method", + "confidence": "high", + "excerpt": "auth method claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_fc13667fe57d80c7", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 17, + "column": 10 + }, + "detector_id": "jwt.issue", + "confidence": "high", + "excerpt": "jsonwebtoken.sign issue call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_7f684ad11256f8c0", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 19, + "column": 5 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_08cd1591819587b4", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 21, + "column": 15 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "ISSUER", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_5fe4ec412b8292a8", + "lifecycle_stage": "issue", + "location": { + "path": "auth.ts", + "line": 22, + "column": 17 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "AUDIENCE", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_7f761cb194aa88ef", + "lifecycle_stage": "expire", + "location": { + "path": "auth.ts", + "line": 23, + "column": 18 + }, + "detector_id": "jwt.attribute.expiration", + "confidence": "high", + "excerpt": "\"15m\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c8db640fa216830b", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 29, + "column": 10 + }, + "detector_id": "jwt.attribute.expiry_enforcement", + "confidence": "low", + "excerpt": "jsonwebtoken.verify enforces exp by library default", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_ef5a75f64cb806d4", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 29, + "column": 10 + }, + "detector_id": "jwt.attribute.signature_verification", + "confidence": "high", + "excerpt": "jsonwebtoken.verify verifies JWT signatures", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_eb240a6458b51f7d", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 29, + "column": 10 + }, + "detector_id": "jwt.validate", + "confidence": "high", + "excerpt": "jsonwebtoken.verify validate call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_ffb17cdf5e5e8b20", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 29, + "column": 28 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2c55d5235636e020", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 30, + "column": 13 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "ISSUER", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2f1f7bb096bf5b6b", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 30, + "column": 13 + }, + "detector_id": "jwt.option.issuer", + "confidence": "high", + "excerpt": "JWT issuer option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_142607e448a7c55a", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 31, + "column": 15 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "AUDIENCE", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_5ff7f5334ab5b971", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 31, + "column": 15 + }, + "detector_id": "jwt.option.audience", + "confidence": "high", + "excerpt": "JWT audience option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_8f9a6b8553ad8d39", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 36, + "column": 10 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "audience is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_5b152f496cd87501", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 36, + "column": 10 + }, + "detector_id": "jwt.attribute.expiry_enforcement", + "confidence": "high", + "excerpt": "JWT expiry enforcement is disabled with ignoreExpiration: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_7d0978c8a0dc497a", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 36, + "column": 10 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "issuer is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_49fd2c2d6edf3ffa", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 36, + "column": 10 + }, + "detector_id": "jwt.attribute.signature_verification", + "confidence": "high", + "excerpt": "jsonwebtoken.verify verifies JWT signatures", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c262b6b3fe4e626d", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 36, + "column": 10 + }, + "detector_id": "jwt.validate", + "confidence": "high", + "excerpt": "jsonwebtoken.verify validate call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_34b40fb747ca2a6e", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 36, + "column": 28 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_3544c272b486ee48", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 36, + "column": 60 + }, + "detector_id": "jwt.option.ignore_expiration", + "confidence": "high", + "excerpt": "true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_5794655114427a01", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 40, + "column": 10 + }, + "detector_id": "jwt.attribute.expiry_enforcement", + "confidence": "high", + "excerpt": "jsonwebtoken.decode does not enforce JWT expiration", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_f60f8290316ad4bb", + "lifecycle_stage": "validate", + "location": { + "path": "auth.ts", + "line": 40, + "column": 10 + }, + "detector_id": "jwt.attribute.signature_verification", + "confidence": "high", + "excerpt": "jsonwebtoken.decode decodes JWTs without signature verification", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1c994198cf6dc30c", + "lifecycle_stage": "introspect", + "location": { + "path": "auth.ts", + "line": 40, + "column": 10 + }, + "detector_id": "jwt.decode_without_verify", + "confidence": "high", + "excerpt": "jsonwebtoken.decode decode_without_verify call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + } + ], + "lifecycle_paths": [ + { + "id": "lifecycle_path_8cc5c35588ac1ebf", + "artifact_ids": [ + "artifact_59b937e65bbea018" + ], + "stages": [ + { + "stage": "issue", + "evidence_ids": [ + "evidence_08cd1591819587b4", + "evidence_0dcaee30a4dba499", + "evidence_0f5a9f0d5b2b4dc0", + "evidence_48517d1eba13bd74", + "evidence_5fe4ec412b8292a8", + "evidence_7e70705f466b78bd", + "evidence_7f684ad11256f8c0", + "evidence_a16bbcfe379030e8", + "evidence_e2260e9f2965c63f", + "evidence_f014933fd3dda9ba", + "evidence_fc13667fe57d80c7" + ] + }, + { + "stage": "validate", + "evidence_ids": [ + "evidence_142607e448a7c55a", + "evidence_2c55d5235636e020", + "evidence_2f1f7bb096bf5b6b", + "evidence_5794655114427a01", + "evidence_5ff7f5334ab5b971", + "evidence_c8db640fa216830b", + "evidence_eb240a6458b51f7d", + "evidence_ef5a75f64cb806d4", + "evidence_f60f8290316ad4bb", + "evidence_ffb17cdf5e5e8b20" + ] + }, + { + "stage": "expire", + "evidence_ids": [ + "evidence_7f761cb194aa88ef" + ] + }, + { + "stage": "introspect", + "evidence_ids": [ + "evidence_1c994198cf6dc30c" + ] + } + ], + "confidence": "low", + "dynamic": false, + "reviewer_question": "Which framework version and deployment settings determine lifecycle behavior for `access_jwt`?" + }, + { + "id": "lifecycle_path_1ffd533199877bad", + "artifact_ids": [ + "artifact_e6e873017238673a" + ], + "stages": [ + { + "stage": "validate", + "evidence_ids": [ + "evidence_34b40fb747ca2a6e", + "evidence_3544c272b486ee48", + "evidence_49fd2c2d6edf3ffa", + "evidence_5b152f496cd87501", + "evidence_7d0978c8a0dc497a", + "evidence_8f9a6b8553ad8d39", + "evidence_c262b6b3fe4e626d" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + } + ], + "findings": [ + { + "id": "finding_f1fcf1961b0774db", + "category": "high_confidence_misconfiguration", + "severity": "high", + "artifact_ids": [ + "artifact_59b937e65bbea018" + ], + "evidence_ids": [ + "evidence_5794655114427a01", + "evidence_c8db640fa216830b" + ], + "title": "JWT `access_jwt` expiry enforcement is disabled or absent", + "description": "Evidence shows this JWT validation path does not enforce expiration.", + "suggested_fix": "Require expiration enforcement when validating JWTs.", + "reviewer_question": "Can expired tokens be accepted on this path?" + }, + { + "id": "finding_ba3477b3bf615bdd", + "category": "high_confidence_misconfiguration", + "severity": "high", + "artifact_ids": [ + "artifact_59b937e65bbea018" + ], + "evidence_ids": [ + "evidence_ef5a75f64cb806d4", + "evidence_f60f8290316ad4bb" + ], + "title": "JWT `access_jwt` is decoded or parsed without signature verification", + "description": "Evidence shows this JWT path does not verify signatures before reading claims.", + "suggested_fix": "Use a verification API with the expected issuer, audience, and signing key before trusting claims.", + "reviewer_question": "Is this decoded JWT used only for non-security-sensitive introspection?" + }, + { + "id": "finding_eb282b5477ced485", + "category": "high_confidence_misconfiguration", + "severity": "high", + "artifact_ids": [ + "artifact_e6e873017238673a" + ], + "evidence_ids": [ + "evidence_5b152f496cd87501" + ], + "title": "JWT `legacy_access_jwt` expiry enforcement is disabled or absent", + "description": "Evidence shows this JWT validation path does not enforce expiration.", + "suggested_fix": "Require expiration enforcement when validating JWTs.", + "reviewer_question": "Can expired tokens be accepted on this path?" + }, + { + "id": "finding_83e5da31912ff3fb", + "category": "missing_validation_evidence", + "severity": "medium", + "artifact_ids": [ + "artifact_e6e873017238673a" + ], + "evidence_ids": [ + "evidence_7d0978c8a0dc497a" + ], + "title": "JWT `legacy_access_jwt` verification has no issuer evidence", + "description": "JWT verification evidence does not include an expected issuer check.", + "suggested_fix": "Require an expected issuer when verifying JWTs.", + "reviewer_question": "Should this service reject tokens with an unexpected issuer?" + }, + { + "id": "finding_90843c170feb8bb3", + "category": "missing_validation_evidence", + "severity": "medium", + "artifact_ids": [ + "artifact_e6e873017238673a" + ], + "evidence_ids": [ + "evidence_8f9a6b8553ad8d39" + ], + "title": "JWT `legacy_access_jwt` verification has no audience evidence", + "description": "JWT verification evidence does not include an expected audience check.", + "suggested_fix": "Require an expected audience when verifying JWTs.", + "reviewer_question": "Should this service reject tokens with an unexpected audience?" + }, + { + "id": "finding_8697abef77df826d", + "category": "framework_default_assumed", + "severity": "medium", + "artifact_ids": [ + "artifact_59b937e65bbea018" + ], + "evidence_ids": [ + "evidence_eb240a6458b51f7d", + "evidence_ffb17cdf5e5e8b20", + "evidence_2c55d5235636e020", + "evidence_142607e448a7c55a", + "evidence_ef5a75f64cb806d4", + "evidence_c8db640fa216830b", + "evidence_5ff7f5334ab5b971", + "evidence_2f1f7bb096bf5b6b", + "evidence_f60f8290316ad4bb", + "evidence_5794655114427a01" + ], + "title": "JWT `access_jwt` validation relies on JWT algorithm defaults", + "description": "JWT verification evidence does not pin accepted algorithms for a library path with historically configuration-dependent `none` handling.", + "suggested_fix": "Pass an explicit safe algorithms allow-list when verifying JWTs.", + "reviewer_question": "Which library version and configuration define the accepted JWT algorithms here?" + }, + { + "id": "finding_ac53ce93423700e5", + "category": "framework_default_assumed", + "severity": "medium", + "artifact_ids": [ + "artifact_e6e873017238673a" + ], + "evidence_ids": [ + "evidence_c262b6b3fe4e626d", + "evidence_34b40fb747ca2a6e", + "evidence_7d0978c8a0dc497a", + "evidence_8f9a6b8553ad8d39", + "evidence_49fd2c2d6edf3ffa", + "evidence_5b152f496cd87501", + "evidence_3544c272b486ee48" + ], + "title": "JWT `legacy_access_jwt` validation relies on JWT algorithm defaults", + "description": "JWT verification evidence does not pin accepted algorithms for a library path with historically configuration-dependent `none` handling.", + "suggested_fix": "Pass an explicit safe algorithms allow-list when verifying JWTs.", + "reviewer_question": "Which library version and configuration define the accepted JWT algorithms here?" + }, + { + "id": "finding_46679a7571b675df", + "category": "missing_validation_evidence", + "severity": "low", + "artifact_ids": [ + "artifact_59b937e65bbea018" + ], + "evidence_ids": [ + "evidence_eb240a6458b51f7d", + "evidence_ffb17cdf5e5e8b20", + "evidence_2c55d5235636e020", + "evidence_142607e448a7c55a", + "evidence_ef5a75f64cb806d4", + "evidence_c8db640fa216830b", + "evidence_5ff7f5334ab5b971", + "evidence_2f1f7bb096bf5b6b", + "evidence_f60f8290316ad4bb", + "evidence_5794655114427a01" + ], + "title": "JWT `access_jwt` verification has no not-before validation evidence", + "description": "JWT verification evidence does not show `nbf` / not-before claim enforcement.", + "suggested_fix": "Require or enforce `nbf` validation where issuer policy uses not-before claims.", + "reviewer_question": "Do issued tokens for this path use `nbf`, and is it enforced in production?" + }, + { + "id": "finding_526caf8537b4fffb", + "category": "missing_validation_evidence", + "severity": "low", + "artifact_ids": [ + "artifact_e6e873017238673a" + ], + "evidence_ids": [ + "evidence_c262b6b3fe4e626d", + "evidence_34b40fb747ca2a6e", + "evidence_7d0978c8a0dc497a", + "evidence_8f9a6b8553ad8d39", + "evidence_49fd2c2d6edf3ffa", + "evidence_5b152f496cd87501", + "evidence_3544c272b486ee48" + ], + "title": "JWT `legacy_access_jwt` verification has no not-before validation evidence", + "description": "JWT verification evidence does not show `nbf` / not-before claim enforcement.", + "suggested_fix": "Require or enforce `nbf` validation where issuer policy uses not-before claims.", + "reviewer_question": "Do issued tokens for this path use `nbf`, and is it enforced in production?" + } + ] +} \ No newline at end of file diff --git a/tests/integration/snapshots/nextjs-route-handler-auth.json b/tests/integration/snapshots/nextjs-route-handler-auth.json new file mode 100644 index 0000000..def5029 --- /dev/null +++ b/tests/integration/snapshots/nextjs-route-handler-auth.json @@ -0,0 +1,2903 @@ +{ + "schema_version": "0.5.0", + "summary": { + "files_discovered": 2, + "files_scanned": 2, + "files_skipped": 0, + "diagnostics": [], + "worker_panic_count": 0 + }, + "files": [ + { + "path": "expected.json", + "language": "json", + "artifacts": [], + "evidence": [], + "diagnostics": [], + "skipped_reason": null + }, + { + "path": "route.ts", + "language": "type_script", + "artifacts": [ + { + "id": "artifact_c561c5272914ef33", + "artifact_type": "access_jwt", + "display_name": "access_jwt", + "locations": [ + { + "path": "route.ts", + "line": 9, + "column": 27 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_1ae3c2d8c685d023", + "evidence_4f85e5391e615c12", + "evidence_84d8e843a7543cf6", + "evidence_a5a7973d4a01f4ee", + "evidence_cb0dbdb2c29a56f3", + "evidence_f8838d112e6014ed" + ], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_6466ba2341dbc9e0" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "jose" + ], + "jwt_attributes": { + "operation": { + "state": "present", + "value": "issue", + "evidence_ids": [ + "evidence_f8838d112e6014ed" + ], + "confidence": "high" + }, + "algorithm": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_84d8e843a7543cf6" + ], + "confidence": "high" + }, + "key_reference": { + "state": "present", + "value": "secret", + "evidence_ids": [ + "evidence_4f85e5391e615c12" + ], + "confidence": "high" + }, + "issuer": { + "state": "present", + "value": "issuer", + "evidence_ids": [ + "evidence_a5a7973d4a01f4ee" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "audience", + "evidence_ids": [ + "evidence_cb0dbdb2c29a56f3" + ], + "confidence": "high" + }, + "expiration": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_6466ba2341dbc9e0" + ], + "confidence": "high" + }, + "signature_verification": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "expiry_enforcement": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "identity_claims": { + "subject": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_1ae3c2d8c685d023" + ], + "confidence": "high" + }, + "user_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "tenant_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "org_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "workspace_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "roles": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "scopes": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "groups": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "email": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "email_verified": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "auth_method": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "auth_class": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + } + } + }, + "token_boundary_attributes": { + "issuer": { + "state": "present", + "value": "issuer", + "evidence_ids": [ + "evidence_a5a7973d4a01f4ee" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "audience", + "evidence_ids": [ + "evidence_cb0dbdb2c29a56f3" + ], + "confidence": "high" + }, + "trust_boundary": { + "state": "present", + "value": "audience", + "evidence_ids": [ + "evidence_cb0dbdb2c29a56f3" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_2b583bc212a31cbe", + "artifact_type": "unknown", + "display_name": "access", + "locations": [ + { + "path": "route.ts", + "line": 16, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_2b583bc212a31cbe", + "evidence_4ab137a7f2af45a3", + "evidence_b4b9c525de641b2f" + ], + "transmit": [ + "evidence_3400b316c2e50e96", + "evidence_5a4a39e105614f12", + "evidence_84f5450bf03616bb", + "evidence_a3a5795ce43fb0cc" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_2423accf73d7423d", + "evidence_8c77344dc14fc373" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs" + ], + "cookie_attributes": { + "http_only": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_b4b9c525de641b2f" + ], + "confidence": "high" + }, + "secure": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_5a4a39e105614f12" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "lax", + "evidence_ids": [ + "evidence_3400b316c2e50e96" + ], + "confidence": "high" + }, + "max_age": { + "state": "missing", + "evidence_ids": [ + "evidence_2423accf73d7423d" + ], + "confidence": "high" + }, + "expires": { + "state": "missing", + "evidence_ids": [ + "evidence_8c77344dc14fc373" + ], + "confidence": "high" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_a3a5795ce43fb0cc" + ], + "confidence": "low" + }, + "domain": { + "state": "missing", + "evidence_ids": [ + "evidence_84f5450bf03616bb" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_ac9b16b63e4ddaff", + "artifact_type": "unknown_token", + "display_name": "unknown_token", + "locations": [ + { + "path": "route.ts", + "line": 26, + "column": 9 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [ + "evidence_ed8bc77a05bef8d8" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "javascript" + ] + }, + { + "id": "artifact_25ceae69a975e8d7", + "artifact_type": "opaque_bearer_token", + "display_name": "authorization_bearer", + "locations": [ + { + "path": "route.ts", + "line": 26, + "column": 17 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [ + "evidence_1b8f65a6befb2a09" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "javascript" + ] + }, + { + "id": "artifact_8fe7fee7047a739e", + "artifact_type": "unknown", + "display_name": null, + "locations": [ + { + "path": "route.ts", + "line": 31, + "column": 9 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [ + "evidence_07678534faf82472", + "evidence_2013faf58dccfe53", + "evidence_293c767a927400d0", + "evidence_9de57872b188a699", + "evidence_cd1e3db180eb769e", + "evidence_e72ec5fd2af8b8d8", + "evidence_e91b8a1a43174135", + "evidence_f542f7654997dc33" + ], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "jose" + ], + "jwt_attributes": { + "operation": { + "state": "present", + "value": "validate", + "evidence_ids": [ + "evidence_f542f7654997dc33" + ], + "confidence": "high" + }, + "algorithm": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "key_reference": { + "state": "present", + "value": "secret", + "evidence_ids": [ + "evidence_e72ec5fd2af8b8d8" + ], + "confidence": "high" + }, + "issuer": { + "state": "present", + "value": "issuer", + "evidence_ids": [ + "evidence_293c767a927400d0" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "audience", + "evidence_ids": [ + "evidence_cd1e3db180eb769e" + ], + "confidence": "high" + }, + "expiration": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "signature_verification": { + "state": "present", + "value": "verified", + "evidence_ids": [ + "evidence_07678534faf82472" + ], + "confidence": "high" + }, + "expiry_enforcement": { + "state": "framework_default", + "value": "jose.jwtVerify default", + "evidence_ids": [ + "evidence_e91b8a1a43174135" + ], + "confidence": "low" + } + }, + "token_boundary_attributes": { + "issuer": { + "state": "present", + "value": "issuer", + "evidence_ids": [ + "evidence_293c767a927400d0" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "audience", + "evidence_ids": [ + "evidence_cd1e3db180eb769e" + ], + "confidence": "high" + }, + "trust_boundary": { + "state": "present", + "value": "audience", + "evidence_ids": [ + "evidence_cd1e3db180eb769e" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_ca5f4ce9912eb5a8", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "route.ts", + "line": 39, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_3b945e587d0c4aa3" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "nextjs" + ] + }, + { + "id": "artifact_dba70235c9632644", + "artifact_type": "unknown", + "display_name": "refresh", + "locations": [ + { + "path": "route.ts", + "line": 40, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_55c92f0ee774b087", + "evidence_994e0b098effac25", + "evidence_dba70235c9632644" + ], + "transmit": [ + "evidence_21d525f460bad55a", + "evidence_3c94a8d36f45549f", + "evidence_6a38024f599f1b26", + "evidence_a2d82c226f347fe6" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_274c001f3d27af39", + "evidence_6ea79ef075fc6f97" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs" + ], + "cookie_attributes": { + "http_only": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_994e0b098effac25" + ], + "confidence": "high" + }, + "secure": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_6a38024f599f1b26" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "strict", + "evidence_ids": [ + "evidence_a2d82c226f347fe6" + ], + "confidence": "high" + }, + "max_age": { + "state": "missing", + "evidence_ids": [ + "evidence_274c001f3d27af39" + ], + "confidence": "high" + }, + "expires": { + "state": "missing", + "evidence_ids": [ + "evidence_6ea79ef075fc6f97" + ], + "confidence": "high" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_21d525f460bad55a" + ], + "confidence": "low" + }, + "domain": { + "state": "missing", + "evidence_ids": [ + "evidence_3c94a8d36f45549f" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_905c4aeebdc48a72", + "artifact_type": "unknown", + "display_name": "logout", + "locations": [ + { + "path": "route.ts", + "line": 48, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_49305a6c8f4a13e2" + ], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "nextjs" + ] + }, + { + "id": "artifact_aff5b3708155b98d", + "artifact_type": "unknown", + "display_name": "logout", + "locations": [ + { + "path": "route.ts", + "line": 48, + "column": 8 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_4117a704272b083d" + ], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "javascript" + ] + }, + { + "id": "artifact_51b218b779323fcd", + "artifact_type": "unknown", + "display_name": "access", + "locations": [ + { + "path": "route.ts", + "line": 49, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_de2617515f383fb9" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs" + ] + }, + { + "id": "artifact_a82d1ab486d9dd80", + "artifact_type": "unknown", + "display_name": "refresh", + "locations": [ + { + "path": "route.ts", + "line": 50, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_272e8be9bb923e24" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs" + ] + } + ], + "evidence": [ + { + "id": "evidence_84d8e843a7543cf6", + "lifecycle_stage": "issue", + "location": { + "path": "route.ts", + "line": 9, + "column": 27 + }, + "detector_id": "jwt.attribute.algorithm", + "confidence": "high", + "excerpt": ".setProtectedHeader({ alg: \"HS256\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_cb0dbdb2c29a56f3", + "lifecycle_stage": "issue", + "location": { + "path": "route.ts", + "line": 9, + "column": 27 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": ".setAudience(audience", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_a5a7973d4a01f4ee", + "lifecycle_stage": "issue", + "location": { + "path": "route.ts", + "line": 9, + "column": 27 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": ".setIssuer(issuer", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_4f85e5391e615c12", + "lifecycle_stage": "issue", + "location": { + "path": "route.ts", + "line": 9, + "column": 27 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1ae3c2d8c685d023", + "lifecycle_stage": "issue", + "location": { + "path": "route.ts", + "line": 9, + "column": 27 + }, + "detector_id": "jwt.attribute.subject", + "confidence": "high", + "excerpt": "subject claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_f8838d112e6014ed", + "lifecycle_stage": "issue", + "location": { + "path": "route.ts", + "line": 9, + "column": 27 + }, + "detector_id": "jwt.issue", + "confidence": "high", + "excerpt": "jose.SignJWT.sign issue call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_6466ba2341dbc9e0", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 9, + "column": 27 + }, + "detector_id": "jwt.attribute.expiration", + "confidence": "high", + "excerpt": ".setExpirationTime(\"15m\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_4ab137a7f2af45a3", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 16, + "column": 3 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2b583bc212a31cbe", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 16, + "column": 3 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": "\n cookies().set(\"access\", [REDACTED], {\n httpOnly: true,", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_84f5450bf03616bb", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 16, + "column": 3 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "high", + "excerpt": "Domain is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_a3a5795ce43fb0cc", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 16, + "column": 3 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for nextjs", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_8c77344dc14fc373", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 16, + "column": 3 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "high", + "excerpt": "Expires is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2423accf73d7423d", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 16, + "column": 3 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "high", + "excerpt": "Max-Age is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_b4b9c525de641b2f", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 17, + "column": 15 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "HttpOnly: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_5a4a39e105614f12", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 18, + "column": 13 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "Secure: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_3400b316c2e50e96", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 19, + "column": 15 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SameSite: \"lax\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_ed8bc77a05bef8d8", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 26, + "column": 9 + }, + "detector_id": "bearer.transmit", + "confidence": "high", + "excerpt": "token = request.headers.get(\"[REDACTED]\")?.replace(\"[REDACTED]\", \"[REDACTED]\")", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1b8f65a6befb2a09", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 26, + "column": 17 + }, + "detector_id": "bearer.read.inbound", + "confidence": "high", + "excerpt": "request.headers.get(\"[REDACTED]\")?.replace(\"[REDACTED]\", \"[REDACTED]\")", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_e91b8a1a43174135", + "lifecycle_stage": "validate", + "location": { + "path": "route.ts", + "line": 31, + "column": 9 + }, + "detector_id": "jwt.attribute.expiry_enforcement", + "confidence": "low", + "excerpt": "jose.jwtVerify enforces exp by library default", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_07678534faf82472", + "lifecycle_stage": "validate", + "location": { + "path": "route.ts", + "line": 31, + "column": 9 + }, + "detector_id": "jwt.attribute.signature_verification", + "confidence": "high", + "excerpt": "jose.jwtVerify verifies JWT signatures", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_f542f7654997dc33", + "lifecycle_stage": "validate", + "location": { + "path": "route.ts", + "line": 31, + "column": 9 + }, + "detector_id": "jwt.validate", + "confidence": "medium", + "excerpt": "jose.jwtVerify validate call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_e72ec5fd2af8b8d8", + "lifecycle_stage": "validate", + "location": { + "path": "route.ts", + "line": 31, + "column": 26 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_293c767a927400d0", + "lifecycle_stage": "validate", + "location": { + "path": "route.ts", + "line": 32, + "column": 5 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "issuer", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2013faf58dccfe53", + "lifecycle_stage": "validate", + "location": { + "path": "route.ts", + "line": 32, + "column": 5 + }, + "detector_id": "jwt.option.issuer", + "confidence": "high", + "excerpt": "JWT issuer option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_cd1e3db180eb769e", + "lifecycle_stage": "validate", + "location": { + "path": "route.ts", + "line": 33, + "column": 5 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "audience", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9de57872b188a699", + "lifecycle_stage": "validate", + "location": { + "path": "route.ts", + "line": 33, + "column": 5 + }, + "detector_id": "jwt.option.audience", + "confidence": "high", + "excerpt": "JWT audience option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_3b945e587d0c4aa3", + "lifecycle_stage": "refresh", + "location": { + "path": "route.ts", + "line": 39, + "column": 1 + }, + "detector_id": "refresh.handler", + "confidence": "medium", + "excerpt": "export async function PATCH() {\n cookies().set(\"[REDACTED]\", \"[REDACTED]\", {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n });\n return Response.json({ refreshed: true });\n}", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_55c92f0ee774b087", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 40, + "column": 3 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_dba70235c9632644", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 40, + "column": 3 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": "export async function PATCH() {\n cookies().set(\"refresh\", [REDACTED], {\n httpOnly: true,", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_3c94a8d36f45549f", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 40, + "column": 3 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "high", + "excerpt": "Domain is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_21d525f460bad55a", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 40, + "column": 3 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for nextjs", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_6ea79ef075fc6f97", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 40, + "column": 3 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "high", + "excerpt": "Expires is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_274c001f3d27af39", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 40, + "column": 3 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "high", + "excerpt": "Max-Age is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_994e0b098effac25", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 41, + "column": 15 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "HttpOnly: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_6a38024f599f1b26", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 42, + "column": 13 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "Secure: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_a2d82c226f347fe6", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 43, + "column": 15 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SameSite: \"strict\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_49305a6c8f4a13e2", + "lifecycle_stage": "revoke", + "location": { + "path": "route.ts", + "line": 48, + "column": 1 + }, + "detector_id": "logout.handler", + "confidence": "medium", + "excerpt": "export async function DELETE() {\n cookies().delete(\"[REDACTED]\");\n cookies().delete(\"[REDACTED]\");\n return new Response(null, { status: 204 });\n}", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_4117a704272b083d", + "lifecycle_stage": "revoke", + "location": { + "path": "route.ts", + "line": 48, + "column": 8 + }, + "detector_id": "logout.handler", + "confidence": "medium", + "excerpt": "async function DELETE() {\n cookies().delete(\"[REDACTED]\");\n cookies().delete(\"[REDACTED]\");\n return new Response(null, { status: 204 });\n}", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_de2617515f383fb9", + "lifecycle_stage": "revoke", + "location": { + "path": "route.ts", + "line": 49, + "column": 3 + }, + "detector_id": "logout.cookie_clear", + "confidence": "high", + "excerpt": "cookies().delete(\"access\")", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_272e8be9bb923e24", + "lifecycle_stage": "revoke", + "location": { + "path": "route.ts", + "line": 50, + "column": 3 + }, + "detector_id": "logout.cookie_clear", + "confidence": "high", + "excerpt": "cookies().delete(\"[REDACTED]\")", + "dynamic": false, + "framework_default": false + } + ], + "diagnostics": [], + "skipped_reason": null + } + ], + "artifacts": [ + { + "id": "artifact_c561c5272914ef33", + "artifact_type": "access_jwt", + "display_name": "access_jwt", + "locations": [ + { + "path": "route.ts", + "line": 9, + "column": 27 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_1ae3c2d8c685d023", + "evidence_4f85e5391e615c12", + "evidence_84d8e843a7543cf6", + "evidence_a5a7973d4a01f4ee", + "evidence_cb0dbdb2c29a56f3", + "evidence_f8838d112e6014ed" + ], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_6466ba2341dbc9e0" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "jose" + ], + "jwt_attributes": { + "operation": { + "state": "present", + "value": "issue", + "evidence_ids": [ + "evidence_f8838d112e6014ed" + ], + "confidence": "high" + }, + "algorithm": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_84d8e843a7543cf6" + ], + "confidence": "high" + }, + "key_reference": { + "state": "present", + "value": "secret", + "evidence_ids": [ + "evidence_4f85e5391e615c12" + ], + "confidence": "high" + }, + "issuer": { + "state": "present", + "value": "issuer", + "evidence_ids": [ + "evidence_a5a7973d4a01f4ee" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "audience", + "evidence_ids": [ + "evidence_cb0dbdb2c29a56f3" + ], + "confidence": "high" + }, + "expiration": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_6466ba2341dbc9e0" + ], + "confidence": "high" + }, + "signature_verification": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "expiry_enforcement": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "identity_claims": { + "subject": { + "state": "present", + "value": "[literal]", + "evidence_ids": [ + "evidence_1ae3c2d8c685d023" + ], + "confidence": "high" + }, + "user_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "tenant_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "org_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "workspace_id": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "roles": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "scopes": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "groups": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "email": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "email_verified": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "auth_method": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "auth_class": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + } + } + }, + "token_boundary_attributes": { + "issuer": { + "state": "present", + "value": "issuer", + "evidence_ids": [ + "evidence_a5a7973d4a01f4ee" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "audience", + "evidence_ids": [ + "evidence_cb0dbdb2c29a56f3" + ], + "confidence": "high" + }, + "trust_boundary": { + "state": "present", + "value": "audience", + "evidence_ids": [ + "evidence_cb0dbdb2c29a56f3" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_2b583bc212a31cbe", + "artifact_type": "unknown", + "display_name": "access", + "locations": [ + { + "path": "route.ts", + "line": 16, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_2b583bc212a31cbe", + "evidence_4ab137a7f2af45a3", + "evidence_b4b9c525de641b2f" + ], + "transmit": [ + "evidence_3400b316c2e50e96", + "evidence_5a4a39e105614f12", + "evidence_84f5450bf03616bb", + "evidence_a3a5795ce43fb0cc" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_2423accf73d7423d", + "evidence_8c77344dc14fc373" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs" + ], + "cookie_attributes": { + "http_only": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_b4b9c525de641b2f" + ], + "confidence": "high" + }, + "secure": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_5a4a39e105614f12" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "lax", + "evidence_ids": [ + "evidence_3400b316c2e50e96" + ], + "confidence": "high" + }, + "max_age": { + "state": "missing", + "evidence_ids": [ + "evidence_2423accf73d7423d" + ], + "confidence": "high" + }, + "expires": { + "state": "missing", + "evidence_ids": [ + "evidence_8c77344dc14fc373" + ], + "confidence": "high" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_a3a5795ce43fb0cc" + ], + "confidence": "low" + }, + "domain": { + "state": "missing", + "evidence_ids": [ + "evidence_84f5450bf03616bb" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_ac9b16b63e4ddaff", + "artifact_type": "unknown_token", + "display_name": "unknown_token", + "locations": [ + { + "path": "route.ts", + "line": 26, + "column": 9 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [ + "evidence_ed8bc77a05bef8d8" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "javascript" + ] + }, + { + "id": "artifact_25ceae69a975e8d7", + "artifact_type": "opaque_bearer_token", + "display_name": "authorization_bearer", + "locations": [ + { + "path": "route.ts", + "line": 26, + "column": 17 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [ + "evidence_1b8f65a6befb2a09" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "javascript" + ] + }, + { + "id": "artifact_8fe7fee7047a739e", + "artifact_type": "unknown", + "display_name": null, + "locations": [ + { + "path": "route.ts", + "line": 31, + "column": 9 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [ + "evidence_07678534faf82472", + "evidence_2013faf58dccfe53", + "evidence_293c767a927400d0", + "evidence_9de57872b188a699", + "evidence_cd1e3db180eb769e", + "evidence_e72ec5fd2af8b8d8", + "evidence_e91b8a1a43174135", + "evidence_f542f7654997dc33" + ], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "jose" + ], + "jwt_attributes": { + "operation": { + "state": "present", + "value": "validate", + "evidence_ids": [ + "evidence_f542f7654997dc33" + ], + "confidence": "high" + }, + "algorithm": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "key_reference": { + "state": "present", + "value": "secret", + "evidence_ids": [ + "evidence_e72ec5fd2af8b8d8" + ], + "confidence": "high" + }, + "issuer": { + "state": "present", + "value": "issuer", + "evidence_ids": [ + "evidence_293c767a927400d0" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "audience", + "evidence_ids": [ + "evidence_cd1e3db180eb769e" + ], + "confidence": "high" + }, + "expiration": { + "state": "unknown", + "evidence_ids": [], + "confidence": "low" + }, + "signature_verification": { + "state": "present", + "value": "verified", + "evidence_ids": [ + "evidence_07678534faf82472" + ], + "confidence": "high" + }, + "expiry_enforcement": { + "state": "framework_default", + "value": "jose.jwtVerify default", + "evidence_ids": [ + "evidence_e91b8a1a43174135" + ], + "confidence": "low" + } + }, + "token_boundary_attributes": { + "issuer": { + "state": "present", + "value": "issuer", + "evidence_ids": [ + "evidence_293c767a927400d0" + ], + "confidence": "high" + }, + "audience": { + "state": "present", + "value": "audience", + "evidence_ids": [ + "evidence_cd1e3db180eb769e" + ], + "confidence": "high" + }, + "trust_boundary": { + "state": "present", + "value": "audience", + "evidence_ids": [ + "evidence_cd1e3db180eb769e" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_ca5f4ce9912eb5a8", + "artifact_type": "unknown", + "display_name": "refresh_token", + "locations": [ + { + "path": "route.ts", + "line": 39, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_3b945e587d0c4aa3" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "nextjs" + ] + }, + { + "id": "artifact_dba70235c9632644", + "artifact_type": "unknown", + "display_name": "refresh", + "locations": [ + { + "path": "route.ts", + "line": 40, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_55c92f0ee774b087", + "evidence_994e0b098effac25", + "evidence_dba70235c9632644" + ], + "transmit": [ + "evidence_21d525f460bad55a", + "evidence_3c94a8d36f45549f", + "evidence_6a38024f599f1b26", + "evidence_a2d82c226f347fe6" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_274c001f3d27af39", + "evidence_6ea79ef075fc6f97" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs" + ], + "cookie_attributes": { + "http_only": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_994e0b098effac25" + ], + "confidence": "high" + }, + "secure": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_6a38024f599f1b26" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "strict", + "evidence_ids": [ + "evidence_a2d82c226f347fe6" + ], + "confidence": "high" + }, + "max_age": { + "state": "missing", + "evidence_ids": [ + "evidence_274c001f3d27af39" + ], + "confidence": "high" + }, + "expires": { + "state": "missing", + "evidence_ids": [ + "evidence_6ea79ef075fc6f97" + ], + "confidence": "high" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_21d525f460bad55a" + ], + "confidence": "low" + }, + "domain": { + "state": "missing", + "evidence_ids": [ + "evidence_3c94a8d36f45549f" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_905c4aeebdc48a72", + "artifact_type": "unknown", + "display_name": "logout", + "locations": [ + { + "path": "route.ts", + "line": 48, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_49305a6c8f4a13e2" + ], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "nextjs" + ] + }, + { + "id": "artifact_aff5b3708155b98d", + "artifact_type": "unknown", + "display_name": "logout", + "locations": [ + { + "path": "route.ts", + "line": 48, + "column": 8 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_4117a704272b083d" + ], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "javascript" + ] + }, + { + "id": "artifact_51b218b779323fcd", + "artifact_type": "unknown", + "display_name": "access", + "locations": [ + { + "path": "route.ts", + "line": 49, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_de2617515f383fb9" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs" + ] + }, + { + "id": "artifact_a82d1ab486d9dd80", + "artifact_type": "unknown", + "display_name": "refresh", + "locations": [ + { + "path": "route.ts", + "line": 50, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_272e8be9bb923e24" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs" + ] + } + ], + "evidence": [ + { + "id": "evidence_84d8e843a7543cf6", + "lifecycle_stage": "issue", + "location": { + "path": "route.ts", + "line": 9, + "column": 27 + }, + "detector_id": "jwt.attribute.algorithm", + "confidence": "high", + "excerpt": ".setProtectedHeader({ alg: \"HS256\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_cb0dbdb2c29a56f3", + "lifecycle_stage": "issue", + "location": { + "path": "route.ts", + "line": 9, + "column": 27 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": ".setAudience(audience", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_a5a7973d4a01f4ee", + "lifecycle_stage": "issue", + "location": { + "path": "route.ts", + "line": 9, + "column": 27 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": ".setIssuer(issuer", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_4f85e5391e615c12", + "lifecycle_stage": "issue", + "location": { + "path": "route.ts", + "line": 9, + "column": 27 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1ae3c2d8c685d023", + "lifecycle_stage": "issue", + "location": { + "path": "route.ts", + "line": 9, + "column": 27 + }, + "detector_id": "jwt.attribute.subject", + "confidence": "high", + "excerpt": "subject claim is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_f8838d112e6014ed", + "lifecycle_stage": "issue", + "location": { + "path": "route.ts", + "line": 9, + "column": 27 + }, + "detector_id": "jwt.issue", + "confidence": "high", + "excerpt": "jose.SignJWT.sign issue call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_6466ba2341dbc9e0", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 9, + "column": 27 + }, + "detector_id": "jwt.attribute.expiration", + "confidence": "high", + "excerpt": ".setExpirationTime(\"15m\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_4ab137a7f2af45a3", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 16, + "column": 3 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2b583bc212a31cbe", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 16, + "column": 3 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": "\n cookies().set(\"access\", [REDACTED], {\n httpOnly: true,", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_84f5450bf03616bb", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 16, + "column": 3 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "high", + "excerpt": "Domain is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_a3a5795ce43fb0cc", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 16, + "column": 3 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for nextjs", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_8c77344dc14fc373", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 16, + "column": 3 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "high", + "excerpt": "Expires is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2423accf73d7423d", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 16, + "column": 3 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "high", + "excerpt": "Max-Age is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_b4b9c525de641b2f", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 17, + "column": 15 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "HttpOnly: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_5a4a39e105614f12", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 18, + "column": 13 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "Secure: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_3400b316c2e50e96", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 19, + "column": 15 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SameSite: \"lax\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_ed8bc77a05bef8d8", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 26, + "column": 9 + }, + "detector_id": "bearer.transmit", + "confidence": "high", + "excerpt": "token = request.headers.get(\"[REDACTED]\")?.replace(\"[REDACTED]\", \"[REDACTED]\")", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_1b8f65a6befb2a09", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 26, + "column": 17 + }, + "detector_id": "bearer.read.inbound", + "confidence": "high", + "excerpt": "request.headers.get(\"[REDACTED]\")?.replace(\"[REDACTED]\", \"[REDACTED]\")", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_e91b8a1a43174135", + "lifecycle_stage": "validate", + "location": { + "path": "route.ts", + "line": 31, + "column": 9 + }, + "detector_id": "jwt.attribute.expiry_enforcement", + "confidence": "low", + "excerpt": "jose.jwtVerify enforces exp by library default", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_07678534faf82472", + "lifecycle_stage": "validate", + "location": { + "path": "route.ts", + "line": 31, + "column": 9 + }, + "detector_id": "jwt.attribute.signature_verification", + "confidence": "high", + "excerpt": "jose.jwtVerify verifies JWT signatures", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_f542f7654997dc33", + "lifecycle_stage": "validate", + "location": { + "path": "route.ts", + "line": 31, + "column": 9 + }, + "detector_id": "jwt.validate", + "confidence": "medium", + "excerpt": "jose.jwtVerify validate call detected with token and key arguments redacted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_e72ec5fd2af8b8d8", + "lifecycle_stage": "validate", + "location": { + "path": "route.ts", + "line": 31, + "column": 26 + }, + "detector_id": "jwt.attribute.key_reference", + "confidence": "high", + "excerpt": "JWT key reference is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_293c767a927400d0", + "lifecycle_stage": "validate", + "location": { + "path": "route.ts", + "line": 32, + "column": 5 + }, + "detector_id": "jwt.attribute.issuer", + "confidence": "high", + "excerpt": "issuer", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2013faf58dccfe53", + "lifecycle_stage": "validate", + "location": { + "path": "route.ts", + "line": 32, + "column": 5 + }, + "detector_id": "jwt.option.issuer", + "confidence": "high", + "excerpt": "JWT issuer option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_cd1e3db180eb769e", + "lifecycle_stage": "validate", + "location": { + "path": "route.ts", + "line": 33, + "column": 5 + }, + "detector_id": "jwt.attribute.audience", + "confidence": "high", + "excerpt": "audience", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_9de57872b188a699", + "lifecycle_stage": "validate", + "location": { + "path": "route.ts", + "line": 33, + "column": 5 + }, + "detector_id": "jwt.option.audience", + "confidence": "high", + "excerpt": "JWT audience option is present", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_3b945e587d0c4aa3", + "lifecycle_stage": "refresh", + "location": { + "path": "route.ts", + "line": 39, + "column": 1 + }, + "detector_id": "refresh.handler", + "confidence": "medium", + "excerpt": "export async function PATCH() {\n cookies().set(\"[REDACTED]\", \"[REDACTED]\", {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n });\n return Response.json({ refreshed: true });\n}", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_55c92f0ee774b087", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 40, + "column": 3 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_dba70235c9632644", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 40, + "column": 3 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": "export async function PATCH() {\n cookies().set(\"refresh\", [REDACTED], {\n httpOnly: true,", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_3c94a8d36f45549f", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 40, + "column": 3 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "high", + "excerpt": "Domain is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_21d525f460bad55a", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 40, + "column": 3 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for nextjs", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_6ea79ef075fc6f97", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 40, + "column": 3 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "high", + "excerpt": "Expires is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_274c001f3d27af39", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 40, + "column": 3 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "high", + "excerpt": "Max-Age is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_994e0b098effac25", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 41, + "column": 15 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "HttpOnly: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_6a38024f599f1b26", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 42, + "column": 13 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "Secure: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_a2d82c226f347fe6", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 43, + "column": 15 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SameSite: \"strict\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_49305a6c8f4a13e2", + "lifecycle_stage": "revoke", + "location": { + "path": "route.ts", + "line": 48, + "column": 1 + }, + "detector_id": "logout.handler", + "confidence": "medium", + "excerpt": "export async function DELETE() {\n cookies().delete(\"[REDACTED]\");\n cookies().delete(\"[REDACTED]\");\n return new Response(null, { status: 204 });\n}", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_4117a704272b083d", + "lifecycle_stage": "revoke", + "location": { + "path": "route.ts", + "line": 48, + "column": 8 + }, + "detector_id": "logout.handler", + "confidence": "medium", + "excerpt": "async function DELETE() {\n cookies().delete(\"[REDACTED]\");\n cookies().delete(\"[REDACTED]\");\n return new Response(null, { status: 204 });\n}", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_de2617515f383fb9", + "lifecycle_stage": "revoke", + "location": { + "path": "route.ts", + "line": 49, + "column": 3 + }, + "detector_id": "logout.cookie_clear", + "confidence": "high", + "excerpt": "cookies().delete(\"access\")", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_272e8be9bb923e24", + "lifecycle_stage": "revoke", + "location": { + "path": "route.ts", + "line": 50, + "column": 3 + }, + "detector_id": "logout.cookie_clear", + "confidence": "high", + "excerpt": "cookies().delete(\"[REDACTED]\")", + "dynamic": false, + "framework_default": false + } + ], + "lifecycle_paths": [ + { + "id": "lifecycle_path_c72f7ffb255f10ab", + "artifact_ids": [ + "artifact_25ceae69a975e8d7" + ], + "stages": [ + { + "stage": "transmit", + "evidence_ids": [ + "evidence_1b8f65a6befb2a09" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_7e0e973b70bff3b6", + "artifact_ids": [ + "artifact_2b583bc212a31cbe", + "artifact_51b218b779323fcd" + ], + "stages": [ + { + "stage": "store", + "evidence_ids": [ + "evidence_2b583bc212a31cbe", + "evidence_4ab137a7f2af45a3", + "evidence_b4b9c525de641b2f" + ] + }, + { + "stage": "transmit", + "evidence_ids": [ + "evidence_3400b316c2e50e96", + "evidence_5a4a39e105614f12", + "evidence_84f5450bf03616bb", + "evidence_a3a5795ce43fb0cc" + ] + }, + { + "stage": "revoke", + "evidence_ids": [ + "evidence_de2617515f383fb9" + ] + }, + { + "stage": "expire", + "evidence_ids": [ + "evidence_2423accf73d7423d", + "evidence_8c77344dc14fc373" + ] + } + ], + "confidence": "low", + "dynamic": false, + "reviewer_question": "Which framework version and deployment settings determine lifecycle behavior for `access`?" + }, + { + "id": "lifecycle_path_aedffd4a2335eeae", + "artifact_ids": [ + "artifact_8fe7fee7047a739e" + ], + "stages": [ + { + "stage": "validate", + "evidence_ids": [ + "evidence_07678534faf82472", + "evidence_2013faf58dccfe53", + "evidence_293c767a927400d0", + "evidence_9de57872b188a699", + "evidence_cd1e3db180eb769e", + "evidence_e72ec5fd2af8b8d8", + "evidence_e91b8a1a43174135", + "evidence_f542f7654997dc33" + ] + } + ], + "confidence": "low", + "dynamic": false, + "reviewer_question": "Which framework version and deployment settings determine lifecycle behavior for `this artifact`?" + }, + { + "id": "lifecycle_path_be40a5c3f5bfce4b", + "artifact_ids": [ + "artifact_905c4aeebdc48a72", + "artifact_aff5b3708155b98d" + ], + "stages": [ + { + "stage": "revoke", + "evidence_ids": [ + "evidence_4117a704272b083d", + "evidence_49305a6c8f4a13e2" + ] + } + ], + "confidence": "medium", + "dynamic": true, + "reviewer_question": "Which production code path determines the effective lifecycle behavior for `logout`?" + }, + { + "id": "lifecycle_path_a63cfe46a8dbbfc4", + "artifact_ids": [ + "artifact_a82d1ab486d9dd80", + "artifact_ca5f4ce9912eb5a8", + "artifact_dba70235c9632644" + ], + "stages": [ + { + "stage": "store", + "evidence_ids": [ + "evidence_55c92f0ee774b087", + "evidence_994e0b098effac25", + "evidence_dba70235c9632644" + ] + }, + { + "stage": "transmit", + "evidence_ids": [ + "evidence_21d525f460bad55a", + "evidence_3c94a8d36f45549f", + "evidence_6a38024f599f1b26", + "evidence_a2d82c226f347fe6" + ] + }, + { + "stage": "refresh", + "evidence_ids": [ + "evidence_3b945e587d0c4aa3" + ] + }, + { + "stage": "revoke", + "evidence_ids": [ + "evidence_272e8be9bb923e24" + ] + }, + { + "stage": "expire", + "evidence_ids": [ + "evidence_274c001f3d27af39", + "evidence_6ea79ef075fc6f97" + ] + } + ], + "confidence": "low", + "dynamic": true, + "reviewer_question": "Which production code path determines the effective lifecycle behavior for `refresh_token`?" + }, + { + "id": "lifecycle_path_5f83f744675da556", + "artifact_ids": [ + "artifact_ac9b16b63e4ddaff" + ], + "stages": [ + { + "stage": "transmit", + "evidence_ids": [ + "evidence_ed8bc77a05bef8d8" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_83c27d68e25040bc", + "artifact_ids": [ + "artifact_c561c5272914ef33" + ], + "stages": [ + { + "stage": "issue", + "evidence_ids": [ + "evidence_1ae3c2d8c685d023", + "evidence_4f85e5391e615c12", + "evidence_84d8e843a7543cf6", + "evidence_a5a7973d4a01f4ee", + "evidence_cb0dbdb2c29a56f3", + "evidence_f8838d112e6014ed" + ] + }, + { + "stage": "expire", + "evidence_ids": [ + "evidence_6466ba2341dbc9e0" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + } + ], + "findings": [ + { + "id": "finding_5c8cd00bb6e07a71", + "category": "missing_validation_evidence", + "severity": "medium", + "artifact_ids": [ + "artifact_25ceae69a975e8d7" + ], + "evidence_ids": [ + "evidence_1b8f65a6befb2a09" + ], + "title": "Inbound token `authorization_bearer` is read without linked validation evidence", + "description": "Inbound bearer/API-key evidence was detected, but no local validation, lookup, or compare evidence was linked for the same token artifact.", + "suggested_fix": "Add or identify source-bound validation evidence before the token is trusted.", + "reviewer_question": "Where is this inbound token checked before it authorizes access?" + }, + { + "id": "finding_b8bc1e32e1309f2e", + "category": "lifecycle_gap", + "severity": "medium", + "artifact_ids": [ + "artifact_c561c5272914ef33" + ], + "evidence_ids": [ + "evidence_1ae3c2d8c685d023", + "evidence_4117a704272b083d", + "evidence_49305a6c8f4a13e2", + "evidence_4f85e5391e615c12", + "evidence_84d8e843a7543cf6", + "evidence_a5a7973d4a01f4ee", + "evidence_cb0dbdb2c29a56f3", + "evidence_f8838d112e6014ed" + ], + "title": "JWT `access_jwt` has logout evidence without linked denylist evidence", + "description": "A logout handler and access-JWT lifecycle evidence were detected in linked source context, but no source-bound denylist, blocklist, or token revocation-store insertion evidence was linked for the same logout flow.", + "suggested_fix": "Insert the JWT identifier into a denylist/blocklist or revoke-store on logout, or document the intentional short-TTL stateless model for reviewer confirmation.", + "reviewer_question": "Where does logout revoke or denylist outstanding `access_jwt` tokens?" + }, + { + "id": "finding_46da9e746d8d6c2c", + "category": "lifecycle_gap", + "severity": "medium", + "artifact_ids": [ + "artifact_c561c5272914ef33" + ], + "evidence_ids": [ + "evidence_1ae3c2d8c685d023", + "evidence_4f85e5391e615c12", + "evidence_84d8e843a7543cf6", + "evidence_a5a7973d4a01f4ee", + "evidence_cb0dbdb2c29a56f3", + "evidence_f8838d112e6014ed" + ], + "title": "JWT `access_jwt` is issued without linked validation evidence", + "description": "JWT issue evidence was linked into a lifecycle path, but no validation evidence was linked for the same artifact.", + "suggested_fix": "Add or identify verification evidence that validates this token before claims are trusted.", + "reviewer_question": "Where is this issued JWT validated before use?" + }, + { + "id": "finding_ffb71adf93936615", + "category": "lifecycle_gap", + "severity": "medium", + "artifact_ids": [ + "artifact_ca5f4ce9912eb5a8" + ], + "evidence_ids": [ + "evidence_3b945e587d0c4aa3", + "evidence_4117a704272b083d", + "evidence_49305a6c8f4a13e2" + ], + "title": "Refresh token `refresh_token` has logout evidence without family revocation", + "description": "Logout and refresh-token lifecycle evidence were detected in linked source context, but no source-bound user-scoped or refresh-family revocation evidence was linked for the logout flow.", + "suggested_fix": "Revoke the user's refresh-token family, delete user-scoped refresh-token records, or remove the refresh-family cache key during logout.", + "reviewer_question": "Where does logout revoke every refresh token in the `refresh_token` family or for the current user?" + }, + { + "id": "finding_2a692dcbf3a18ab6", + "category": "missing_validation_evidence", + "severity": "low", + "artifact_ids": [ + "artifact_8fe7fee7047a739e" + ], + "evidence_ids": [ + "evidence_f542f7654997dc33", + "evidence_e72ec5fd2af8b8d8", + "evidence_293c767a927400d0", + "evidence_cd1e3db180eb769e", + "evidence_07678534faf82472", + "evidence_e91b8a1a43174135", + "evidence_9de57872b188a699", + "evidence_2013faf58dccfe53" + ], + "title": "JWT `unknown_jwt` verification has no not-before validation evidence", + "description": "JWT verification evidence does not show `nbf` / not-before claim enforcement.", + "suggested_fix": "Require or enforce `nbf` validation where issuer policy uses not-before claims.", + "reviewer_question": "Do issued tokens for this path use `nbf`, and is it enforced in production?" + }, + { + "id": "finding_a328bbb5ff785088", + "category": "lifecycle_gap", + "severity": "low", + "artifact_ids": [ + "artifact_2b583bc212a31cbe" + ], + "evidence_ids": [ + "evidence_de2617515f383fb9" + ], + "title": "Cookie `access` is cleared on logout without linked server-side revocation", + "description": "Logout evidence clears a client-side cookie, but no linked server-side session, token, or provider revocation evidence was found for the same lifecycle path.", + "suggested_fix": "Invalidate the server-side session or refresh token in addition to deleting the browser cookie.", + "reviewer_question": "Where is the server-side session or token behind `access` revoked during logout?" + }, + { + "id": "finding_2673ca01a7369322", + "category": "lifecycle_gap", + "severity": "low", + "artifact_ids": [ + "artifact_ca5f4ce9912eb5a8" + ], + "evidence_ids": [ + "evidence_272e8be9bb923e24" + ], + "title": "Cookie `refresh_token` is cleared on logout without linked server-side revocation", + "description": "Logout evidence clears a client-side cookie, but no linked server-side session, token, or provider revocation evidence was found for the same lifecycle path.", + "suggested_fix": "Invalidate the server-side session or refresh token in addition to deleting the browser cookie.", + "reviewer_question": "Where is the server-side session or token behind `refresh_token` revoked during logout?" + }, + { + "id": "finding_b6da82e39f1b36e8", + "category": "dynamic_review_required", + "severity": "low", + "artifact_ids": [ + "artifact_ac9b16b63e4ddaff" + ], + "evidence_ids": [ + "evidence_ed8bc77a05bef8d8" + ], + "title": "Token `unknown_token` has provider-managed or dynamic handling", + "description": "Bearer/API-key behavior appears dynamic, provider-managed, config-driven, or server-to-server only, so the static evidence should be reviewed before treating lifecycle controls as deterministic.", + "suggested_fix": "Document the provider, wrapper, or server-side policy for issuance, storage, validation, scope, expiry, rotation, and revocation.", + "reviewer_question": "Which runtime configuration or provider settings govern this token lifecycle?" + }, + { + "id": "finding_b7a7e921d36f9cf5", + "category": "dynamic_review_required", + "severity": "low", + "artifact_ids": [ + "artifact_ca5f4ce9912eb5a8" + ], + "evidence_ids": [ + "evidence_3b945e587d0c4aa3" + ], + "title": "Token `refresh_token` has dynamic refresh behavior without linked revocation evidence", + "description": "Refresh lifecycle evidence appears provider-managed or dynamic, and no deterministic source-bound revoke or rotation evidence was linked for the same artifact.", + "suggested_fix": "Confirm the provider or runtime refresh policy rotates or revokes previous refresh tokens.", + "reviewer_question": "Which provider setting or runtime path revokes previous refresh tokens for `refresh_token`?" + }, + { + "id": "finding_a2856514b9437d76", + "category": "framework_default_assumed", + "severity": "low", + "artifact_ids": [ + "artifact_8fe7fee7047a739e" + ], + "evidence_ids": [ + "evidence_e91b8a1a43174135" + ], + "title": "JWT `unknown_jwt` expiry enforcement relies on library defaults", + "description": "JWT validation appears to rely on the library default for expiration enforcement.", + "suggested_fix": "Make expiration enforcement explicit or document the library version and default.", + "reviewer_question": "Which JWT library version and settings determine expiration enforcement here?" + } + ] +} \ No newline at end of file