Compare commits

...
57 changed files with 307 additions and 68 deletions
+12
View File
@@ -4,6 +4,18 @@ All notable changes to the **Prowler API** are documented in this file.
<!-- changelog: release notes start -->
## [1.42.0] (Prowler v5.41.0)
### 🚀 Added
- Jira issues created from Prowler Cloud now carry the `prowler`, `prowler-<provider>`, `prowler-<severity>`, `prowler-<check-id>`, and `prowler-finding-<finding-uid>` labels, a link back to the finding when `DJANGO_UI_BASE_URL` is configured, and the tenant name [(#12540)](https://github.com/prowler-cloud/prowler/pull/12540)
### 🐞 Fixed
- `POST /api/v1/mute-rules` now updates only each affected provider's latest completed scan and future scans, preventing historical reaggregation from flooding Celery queues [(#12681)](https://github.com/prowler-cloud/prowler/pull/12681)
---
## [1.41.0] (Prowler v5.40.0)
### 🐞 Fixed
@@ -1 +0,0 @@
Jira issues created from Prowler Cloud now carry the `prowler`, `prowler-<provider>`, `prowler-<severity>`, `prowler-<check-id>`, and `prowler-finding-<finding-uid>` labels, a link back to the finding when `DJANGO_UI_BASE_URL` is configured, and the tenant name
@@ -1 +0,0 @@
`POST /api/v1/mute-rules` now updates only each affected provider's latest completed scan and future scans, preventing historical reaggregation from flooding Celery queues
+1 -1
View File
@@ -45,7 +45,7 @@ dependencies = [
"gunicorn==26.0.0",
"uvloop==0.22.1",
"lxml==6.1.0",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@master",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.41",
"psycopg2-binary==2.9.9",
"pytest-celery[redis] (==1.3.0)",
"sentry-sdk[django] (==2.56.0)",
Generated
+2 -2
View File
@@ -4836,7 +4836,7 @@ wheels = [
[[package]]
name = "prowler"
version = "5.41.0"
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=master#f05a490cd74a2c0f11a5d66d8ce29d03fa5c64a2" }
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=v5.41#18453e592e0a2550c726a754fda79870de428abf" }
dependencies = [
{ name = "alibabacloud-actiontrail20200706" },
{ name = "alibabacloud-credentials" },
@@ -5038,7 +5038,7 @@ requires-dist = [
{ name = "matplotlib", specifier = "==3.10.8" },
{ name = "neo4j", specifier = "==6.1.0" },
{ name = "openai", specifier = "==1.109.1" },
{ name = "prowler", git = "https://github.com/prowler-cloud/prowler.git?rev=master" },
{ name = "prowler", git = "https://github.com/prowler-cloud/prowler.git?rev=v5.41" },
{ name = "psycopg2-binary", specifier = "==2.9.9" },
{ name = "pytest-celery", extras = ["redis"], specifier = "==1.3.0" },
{ name = "reportlab", specifier = "==4.4.10" },
+17
View File
@@ -4,6 +4,23 @@ All notable changes to the **Prowler MCP Server** are documented in this file.
<!-- changelog: release notes start -->
## [0.12.0] (Prowler v5.41.0)
### 🚀 Added
- Prowler App tools now report a failure as an MCP tool execution error (`isError: true`, explanation in `content`) instead of as a successful result carrying an `{"error": ...}` object, which clients and models read as a success [(#12532)](https://github.com/prowler-cloud/prowler/pull/12532)
### 🔄 Changed
- `prowler_get_compliance_framework_state_details` now rejects a call that passes both `scan_id` and `provider_id` instead of silently ignoring the provider, which could report on a scan belonging to a different provider than the one that was asked about [(#12532)](https://github.com/prowler-cloud/prowler/pull/12532)
### 🐞 Fixed
- `prowler_hub_get_check_code` and `prowler_hub_get_check_fixer` now report a check ID that belongs to another provider as such, naming that provider, instead of reporting the ID as one that does not exist [(#12533)](https://github.com/prowler-cloud/prowler/pull/12533)
- `prowler_docs_search` no longer reports a failed search as zero matches or an unreadable answer as a bad search term, and `prowler_docs_get_document` no longer reports a failed fetch as a missing page [(#12534)](https://github.com/prowler-cloud/prowler/pull/12534)
---
## [0.11.0] (Prowler v5.40.0)
### 🚀 Added
@@ -1 +0,0 @@
Prowler App tools now report a failure as an MCP tool execution error (`isError: true`, explanation in `content`) instead of as a successful result carrying an `{"error": ...}` object, which clients and models read as a success
@@ -1 +0,0 @@
`prowler_get_compliance_framework_state_details` now rejects a call that passes both `scan_id` and `provider_id` instead of silently ignoring the provider, which could report on a scan belonging to a different provider than the one that was asked about
@@ -1 +0,0 @@
`prowler_docs_search` no longer reports a failed search as zero matches or an unreadable answer as a bad search term, and `prowler_docs_get_document` no longer reports a failed fetch as a missing page
@@ -1 +0,0 @@
`prowler_hub_get_check_code` and `prowler_hub_get_check_fixer` now report a check ID that belongs to another provider as such, naming that provider, instead of reporting the ID as one that does not exist
+43
View File
@@ -4,6 +4,49 @@ All notable changes to the **Prowler SDK** are documented in this file.
<!-- changelog: release notes start -->
## [5.41.0] (Prowler v5.41.0)
### 🚀 Added
- `memorydb_cluster_in_transit_encryption_enabled` check for AWS provider, verifying MemoryDB clusters have in-transit encryption (TLS) enabled [(#12246)](https://github.com/prowler-cloud/prowler/pull/12246)
- `elasticbeanstalk_environment_no_secrets_in_configuration` check for AWS provider, scanning the option settings of every Elastic Beanstalk environment for hardcoded secrets [(#12378)](https://github.com/prowler-cloud/prowler/pull/12378)
- CIS Google Workspace Foundations Benchmark v1.4.0 compliance framework [(#12513)](https://github.com/prowler-cloud/prowler/pull/12513)
- `Jira.send_finding()` returns typed creation outcomes with the issue key, immutable ID and browse URL, and supports length-safe finding and delivery-attempt labels plus marker lookup for uncertain deliveries [(#12539)](https://github.com/prowler-cloud/prowler/pull/12539)
- `Jira.get_issues_status()` resolves issue references by immutable ID in batches and returns explicit open, done, moved, missing, forbidden or unknown outcomes without treating missing issues as deleted [(#12539)](https://github.com/prowler-cloud/prowler/pull/12539)
- `guardduty_ai_protection_enabled` check for AWS provider, flagging GuardDuty detectors without AI Protection, which analyzes AWS CloudTrail data events from Amazon Bedrock, Amazon Bedrock AgentCore and Amazon SageMaker AI; a detector that does not report the feature is `MANUAL` rather than `FAIL` [(#12564)](https://github.com/prowler-cloud/prowler/pull/12564)
- `guardduty_runtime_monitoring_enabled` check for AWS provider, flagging GuardDuty detectors without unified Runtime Monitoring, the only feature that covers Amazon EC2 instances and Amazon ECS on AWS Fargate tasks in addition to Amazon EKS [(#12564)](https://github.com/prowler-cloud/prowler/pull/12564)
- `ecr_registry_enhanced_scanning_enabled` check for AWS provider, verifying that the ECR registry scan type is enhanced (Amazon Inspector, covering programming language packages and continuous rescanning) instead of basic, and reporting MANUAL when the registry scanning configuration cannot be read [(#12660)](https://github.com/prowler-cloud/prowler/pull/12660)
- `eks_cluster_vpc_cni_network_policy_enforced` check for AWS provider, flagging EKS clusters whose Amazon VPC CNI managed add-on does not enable Kubernetes network policy enforcement, and reporting MANUAL where the EKS API cannot show the setting [(#12661)](https://github.com/prowler-cloud/prowler/pull/12661)
- `cloudwatch_log_group_agentcore_data_protection_policy_enabled` check for AWS provider, verifying that Bedrock AgentCore log groups mask sensitive data with a CloudWatch Logs data protection policy [(#12662)](https://github.com/prowler-cloud/prowler/pull/12662)
- `iam_policy_no_agentcore_workload_access_token_wildcard` check for AWS provider, flagging customer-managed IAM policies that allow `bedrock-agentcore:GetWorkloadAccessToken`, `GetWorkloadAccessTokenForJWT` or `GetWorkloadAccessTokenForUserId` on resources reaching workload identities other than the caller's own, which AWS documents as the only binding on the unverified user ID the token is issued for [(#12664)](https://github.com/prowler-cloud/prowler/pull/12664)
- `iam_policy_passrole_to_bedrock_agentcore_restricted` check for AWS provider, flagging customer-managed IAM policies that allow `iam:PassRole` over every role where the passed role can reach Bedrock AgentCore, so any principal holding the policy can run agent code under any role in the account [(#12664)](https://github.com/prowler-cloud/prowler/pull/12664)
- `iam_role_service_trust_restricts_source_to_account` check for AWS provider, flagging IAM roles whose trust policy lets an AWS service principal assume the role without confining the request to a specific source account, including trust policies that `iam_role_cross_service_confused_deputy_prevention` does not evaluate [(#12664)](https://github.com/prowler-cloud/prowler/pull/12664)
- `PROWLER_IMAGE_PROVIDER_ALLOWED_PRIVATE_NETWORKS` environment variable so the image provider can reach container registries on allowlisted private networks, keeping every other non-public address blocked [(#12678)](https://github.com/prowler-cloud/prowler/pull/12678)
- `PROWLER_AWS_PARTITION` environment variable to select the AWS partition used for STS credential validation and scan bootstrap, with a clear error when the account belongs to a different partition [(#12680)](https://github.com/prowler-cloud/prowler/pull/12680)
### 🔄 Changed
- Google Workspace checks mapped to CIS evaluate the full audit procedure instead of a single condition, so Gmail spoofing actions, 2-Step Verification, password expiration and alert severity left on Google's defaults no longer pass [(#12513)](https://github.com/prowler-cloud/prowler/pull/12513)
- `security_login_challenges_configured` and `security_2sv_enforced` unmapped from CIS Google Workspace 4.1.4.1 and CISA SCuBA 0.6 `GWS.COMMONCONTROLS.1.1`, whose Post-SSO verification and phishing-resistant MFA requirements neither check can prove [(#12513)](https://github.com/prowler-cloud/prowler/pull/12513)
### 🐞 Fixed
- GitHub repository discovery for unscoped scans now paginates beyond the first 100 accessible repositories instead of silently scanning only the first page [(#12460)](https://github.com/prowler-cloud/prowler/pull/12460)
- `rules_*_alert_configured` checks no longer pass a rule whose delivery to the alert center is disabled, the setting behind the benchmark's "Ensure that Alerts is set to On" [(#12513)](https://github.com/prowler-cloud/prowler/pull/12513)
- `security_password_policy_strong` no longer fails a domain that never touched the password strength setting: Google enforces strong passwords by default, so an unset value is the secure default and not a missing configuration [(#12513)](https://github.com/prowler-cloud/prowler/pull/12513)
- `security_2sv_enforced` and `security_2sv_hardware_keys_admins` report MANUAL instead of judging domain-wide values that a group or a sub-organizational unit overrides, or that were dropped because the root organizational unit could not be resolved; a domain-wide failure is still reported as such, with the override noted [(#12513)](https://github.com/prowler-cloud/prowler/pull/12513)
- `ecr_registry_scan_images_on_push_enabled` no longer passes a registry whose scanning rules are all `MANUAL`, nor describes a `CONTINUOUS_SCAN` registry as scanning on push; each rule's `scanFrequency` is now read instead of inferred from a rule's presence [(#12560)](https://github.com/prowler-cloud/prowler/pull/12560)
- CloudWatch log metric filter checks no longer crash with `AttributeError` when the account has a metric filter whose log group was not retrieved [(#12561)](https://github.com/prowler-cloud/prowler/pull/12561)
- `guardduty_eks_runtime_monitoring_enabled` no longer reports `FAIL` for detectors that use unified Runtime Monitoring; the GuardDuty service now reads the `RUNTIME_MONITORING` feature, which is mutually exclusive with `EKS_RUNTIME_MONITORING` and already covers Amazon EKS [(#12564)](https://github.com/prowler-cloud/prowler/pull/12564)
- Checks no longer report `FAIL` when the scanning identity lacks permissions, an API is not enabled or a feature is not licensed; they now emit a single `MANUAL` finding naming what is required, across 28 M365, Azure, AWS and GCP checks [(#12645)](https://github.com/prowler-cloud/prowler/pull/12645)
- `sagemaker_notebook_instance_without_direct_internet_access_configured` check logic to read the `DirectInternetAccess` setting instead of `RootAccess`, failing a notebook instance with direct internet access enabled even when root access is disabled [(#12659)](https://github.com/prowler-cloud/prowler/pull/12659)
- Basic authentication fallback in the image provider when a registry rejects the negotiated bearer token, so registries like Harbor that guard catalog listing behind Basic can be enumerated [(#12678)](https://github.com/prowler-cloud/prowler/pull/12678)
- Registry catalog listing when the server answers with a Bearer challenge after negotiating Basic (or anonymous) authentication, switching to a bearer token obtained from the challenge instead of failing [(#12678)](https://github.com/prowler-cloud/prowler/pull/12678)
- `--registry-insecure` now propagates to Trivy via `TRIVY_INSECURE`, so images in registries with self-signed certificates can be pulled and scanned, not just enumerated [(#12678)](https://github.com/prowler-cloud/prowler/pull/12678)
- Registry scans in the Image provider now skip non-image OCI artifacts (Helm charts, cosign signatures, SBOM attestations) and no longer abort the whole scan when Trivy fails on a single discovered image; registry enumeration also runs in parallel instead of one request at a time [(#12695)](https://github.com/prowler-cloud/prowler/pull/12695)
---
## [5.40.0] (Prowler v5.40.0)
### 🚀 Added
@@ -1 +0,0 @@
`PROWLER_AWS_PARTITION` environment variable to select the AWS partition used for STS credential validation and scan bootstrap, with a clear error when the account belongs to a different partition
@@ -1 +0,0 @@
CIS Google Workspace Foundations Benchmark v1.4.0 compliance framework
@@ -1 +0,0 @@
`cloudwatch_log_group_agentcore_data_protection_policy_enabled` check for AWS provider, verifying that Bedrock AgentCore log groups mask sensitive data with a CloudWatch Logs data protection policy
@@ -1 +0,0 @@
CloudWatch log metric filter checks no longer crash with `AttributeError` when the account has a metric filter whose log group was not retrieved
@@ -1 +0,0 @@
`ecr_registry_enhanced_scanning_enabled` check for AWS provider, verifying that the ECR registry scan type is enhanced (Amazon Inspector, covering programming language packages and continuous rescanning) instead of basic, and reporting MANUAL when the registry scanning configuration cannot be read
@@ -1 +0,0 @@
`ecr_registry_scan_images_on_push_enabled` no longer passes a registry whose scanning rules are all `MANUAL`, nor describes a `CONTINUOUS_SCAN` registry as scanning on push; each rule's `scanFrequency` is now read instead of inferred from a rule's presence
@@ -1 +0,0 @@
`eks_cluster_vpc_cni_network_policy_enforced` check for AWS provider, flagging EKS clusters whose Amazon VPC CNI managed add-on does not enable Kubernetes network policy enforcement, and reporting MANUAL where the EKS API cannot show the setting
@@ -1 +0,0 @@
`elasticbeanstalk_environment_no_secrets_in_configuration` check for AWS provider, scanning the option settings of every Elastic Beanstalk environment for hardcoded secrets
@@ -1 +0,0 @@
GitHub repository discovery for unscoped scans now paginates beyond the first 100 accessible repositories instead of silently scanning only the first page
@@ -1 +0,0 @@
`security_2sv_enforced` and `security_2sv_hardware_keys_admins` report MANUAL instead of judging domain-wide values that a group or a sub-organizational unit overrides, or that were dropped because the root organizational unit could not be resolved; a domain-wide failure is still reported as such, with the override noted
@@ -1 +0,0 @@
`rules_*_alert_configured` checks no longer pass a rule whose delivery to the alert center is disabled, the setting behind the benchmark's "Ensure that Alerts is set to On"
@@ -1 +0,0 @@
Google Workspace checks mapped to CIS evaluate the full audit procedure instead of a single condition, so Gmail spoofing actions, 2-Step Verification, password expiration and alert severity left on Google's defaults no longer pass
@@ -1 +0,0 @@
`security_login_challenges_configured` and `security_2sv_enforced` unmapped from CIS Google Workspace 4.1.4.1 and CISA SCuBA 0.6 `GWS.COMMONCONTROLS.1.1`, whose Post-SSO verification and phishing-resistant MFA requirements neither check can prove
@@ -1 +0,0 @@
`security_password_policy_strong` no longer fails a domain that never touched the password strength setting: Google enforces strong passwords by default, so an unset value is the secure default and not a missing configuration
@@ -1 +0,0 @@
`guardduty_ai_protection_enabled` check for AWS provider, flagging GuardDuty detectors without AI Protection, which analyzes AWS CloudTrail data events from Amazon Bedrock, Amazon Bedrock AgentCore and Amazon SageMaker AI; a detector that does not report the feature is `MANUAL` rather than `FAIL`
@@ -1 +0,0 @@
`guardduty_runtime_monitoring_enabled` check for AWS provider, flagging GuardDuty detectors without unified Runtime Monitoring, the only feature that covers Amazon EC2 instances and Amazon ECS on AWS Fargate tasks in addition to Amazon EKS
@@ -1 +0,0 @@
`guardduty_eks_runtime_monitoring_enabled` no longer reports `FAIL` for detectors that use unified Runtime Monitoring; the GuardDuty service now reads the `RUNTIME_MONITORING` feature, which is mutually exclusive with `EKS_RUNTIME_MONITORING` and already covers Amazon EKS
@@ -1 +0,0 @@
`iam_policy_passrole_to_bedrock_agentcore_restricted` check for AWS provider, flagging customer-managed IAM policies that allow `iam:PassRole` over every role where the passed role can reach Bedrock AgentCore, so any principal holding the policy can run agent code under any role in the account
@@ -1 +0,0 @@
`iam_policy_no_agentcore_workload_access_token_wildcard` check for AWS provider, flagging customer-managed IAM policies that allow `bedrock-agentcore:GetWorkloadAccessToken`, `GetWorkloadAccessTokenForJWT` or `GetWorkloadAccessTokenForUserId` on resources reaching workload identities other than the caller's own, which AWS documents as the only binding on the unverified user ID the token is issued for
@@ -1 +0,0 @@
`iam_role_service_trust_restricts_source_to_account` check for AWS provider, flagging IAM roles whose trust policy lets an AWS service principal assume the role without confining the request to a specific source account, including trust policies that `iam_role_cross_service_confused_deputy_prevention` does not evaluate
@@ -1 +0,0 @@
`PROWLER_IMAGE_PROVIDER_ALLOWED_PRIVATE_NETWORKS` environment variable so the image provider can reach container registries on allowlisted private networks, keeping every other non-public address blocked
@@ -1 +0,0 @@
Basic authentication fallback in the image provider when a registry rejects the negotiated bearer token, so registries like Harbor that guard catalog listing behind Basic can be enumerated
@@ -1 +0,0 @@
Registry catalog listing when the server answers with a Bearer challenge after negotiating Basic (or anonymous) authentication, switching to a bearer token obtained from the challenge instead of failing
@@ -1 +0,0 @@
`--registry-insecure` now propagates to Trivy via `TRIVY_INSECURE`, so images in registries with self-signed certificates can be pulled and scanned, not just enumerated
@@ -1 +0,0 @@
Registry scans in the Image provider now skip non-image OCI artifacts (Helm charts, cosign signatures, SBOM attestations) and no longer abort the whole scan when Trivy fails on a single discovered image; registry enumeration also runs in parallel instead of one request at a time
@@ -1 +0,0 @@
`Jira.send_finding()` returns typed creation outcomes with the issue key, immutable ID and browse URL, and supports length-safe finding and delivery-attempt labels plus marker lookup for uncertain deliveries
@@ -1 +0,0 @@
`Jira.get_issues_status()` resolves issue references by immutable ID in batches and returns explicit open, done, moved, missing, forbidden or unknown outcomes without treating missing issues as deleted
@@ -1 +0,0 @@
`memorydb_cluster_in_transit_encryption_enabled` check for AWS provider, verifying MemoryDB clusters have in-transit encryption (TLS) enabled
@@ -1 +0,0 @@
Checks no longer report `FAIL` when the scanning identity lacks permissions, an API is not enabled or a feature is not licensed; they now emit a single `MANUAL` finding naming what is required, across 28 M365, Azure, AWS and GCP checks
@@ -1 +0,0 @@
`sagemaker_notebook_instance_without_direct_internet_access_configured` check logic to read the `DirectInternetAccess` setting instead of `RootAccess`, failing a notebook instance with direct internet access enabled even when root access is disabled
+18
View File
@@ -4,6 +4,24 @@ All notable changes to the **Prowler UI** are documented in this file.
<!-- changelog: release notes start -->
## [1.41.0] (Prowler v5.41.0)
### 🚀 Added
- Finding-report imports from Scans for Cloud and Private Cloud deployments [(#12554)](https://github.com/prowler-cloud/prowler/pull/12554)
- Slack integration: the connection check leaves its result on the card instead of only in a toast, naming the channel Slack refused or the channels it reached (Prowler Cloud only) [(#12677)](https://github.com/prowler-cloud/prowler/pull/12677)
### 🔄 Changed
- Slack integration: the bot is referred to as `@Prowler Cloud`, identifiers are set as inline code, and the private-channel hint links to its docs (Prowler Cloud only) [(#12677)](https://github.com/prowler-cloud/prowler/pull/12677)
- Slack integration: a selected private channel carries the same visible `Private` badge as the channel list, replacing the lock icon on the chip (Prowler Cloud only) [(#12677)](https://github.com/prowler-cloud/prowler/pull/12677)
### 🐞 Fixed
- Cached permissions now refresh from `/users/me?include=roles` after access token rotation [(#12640)](https://github.com/prowler-cloud/prowler/pull/12640)
---
## [1.40.0] (Prowler v5.40.0)
### 🚀 Added
@@ -1 +0,0 @@
Finding-report imports from Scans for Cloud and Private Cloud deployments
@@ -1 +0,0 @@
Cached permissions now refresh from `/users/me?include=roles` after access token rotation
@@ -1 +0,0 @@
Slack integration: the connection check leaves its result on the card instead of only in a toast, naming the channel Slack refused or the channels it reached (Prowler Cloud only)
@@ -1 +0,0 @@
Slack integration: a selected private channel carries the same visible `Private` badge as the channel list, replacing the lock icon on the chip (Prowler Cloud only)
@@ -1 +0,0 @@
Slack integration: the bot is referred to as `@Prowler Cloud`, identifiers are set as inline code, and the private-channel hint links to its docs (Prowler Cloud only)
@@ -0,0 +1 @@
Scan Jobs onboarding tour no longer targets an unmounted In Progress row from other tabs
@@ -2,6 +2,7 @@ import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { OnboardingFlow } from "@/lib/onboarding";
import { useScansStore } from "@/store";
import { ProviderProps } from "@/types";
@@ -115,6 +116,18 @@ vi.mock("@/components/onboarding", () => ({
PageReady: () => <div data-testid="page-ready" />,
}));
interface OnboardingTriggerProps {
flow: OnboardingFlow;
}
const getTriggeredTourTargets = () => {
const triggerProps = onboardingTriggerSpy.mock.calls.at(-1)?.[0] as
| OnboardingTriggerProps
| undefined;
return triggerProps?.flow.tour.steps.map((step) => step.target);
};
const providers: ProviderProps[] = [
{
id: "provider-1",
@@ -594,6 +607,50 @@ describe("ScansPageShell", () => {
expect(screen.getByTestId("onboarding-trigger")).toBeInTheDocument();
});
it("uses only mounted tour targets when an active scan exists on the completed tab", () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "false");
searchParamsValue.current = "tab=completed";
// When
render(
<ScansPageShell
providers={providers}
hasManageScansPermission
activeScanCount={1}
>
<div>Scans table</div>
</ScansPageShell>,
);
// Then
expect(getTriggeredTourTargets()).toEqual([undefined, "launch", "tabs"]);
});
it("targets the running scan when its row is mounted on the in progress tab", () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "false");
searchParamsValue.current = "tab=active";
// When
render(
<ScansPageShell
providers={providers}
hasManageScansPermission
activeScanCount={1}
>
<div>Scans table</div>
</ScansPageShell>,
);
// Then
expect(getTriggeredTourTargets()).toEqual([
undefined,
"in-progress",
"launch",
]);
});
it("suppresses the view-first-scan tour when no provider is connected, since Launch Scan is disabled", () => {
vi.stubEnv("UI_CLOUD_ENABLED", "false");
+5 -4
View File
@@ -80,9 +80,10 @@ export function ScansPageShell({
const launchDisabled = !hasManageScansPermission || !hasConnectedProviders;
const launchOpen =
!launchDisabled && (isLaunchScanModalOpen || urlLaunchOpen);
// When a scan is already running, the tour highlights its row (anchored in
// ScanJobsTable); otherwise it falls back to the Launch Scan button + tabs.
const hasInProgressScan = activeScanCount > 0;
// ScanJobsTable only mounts the in-progress row anchor on the active tab.
// Other tabs use the fallback tour so every target exists in the current DOM.
const hasVisibleInProgressScan =
activeScanCount > 0 && filters.activeTab === SCAN_JOBS_TAB.ACTIVE;
const getTabLabel = (tab: ScanJobsTab) => {
const label = SCAN_TAB_LABELS[tab];
@@ -122,7 +123,7 @@ export function ScansPageShell({
<OnboardingTrigger
flow={{
...viewFirstScanFlow,
tour: buildViewFirstScanTour(hasInProgressScan),
tour: buildViewFirstScanTour(hasVisibleInProgressScan),
}}
/>
</Suspense>
@@ -1,5 +1,5 @@
import { render } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { addProviderTour } from "../add-provider.tour";
import type { TourCompletionRecord } from "../tour-types";
@@ -94,3 +94,64 @@ describe("adaptStep autoAdvance", () => {
expect(driveStep.popover?.showButtons).toBeUndefined();
});
});
describe("adaptStep selector fallback", () => {
afterEach(() => {
document.body.replaceChildren();
});
it("uses the fallback target when the primary target disappears", () => {
// Given
const fallbackElement = document.createElement("div");
fallbackElement.dataset.tourId = "view-first-scan-tabs";
document.body.append(fallbackElement);
const driveStep = adaptStep("view-first-scan", {
target: "in-progress",
fallbackTarget: "tabs",
title: "Your scan is running",
});
// When
const resolvedElement = (driveStep.element as () => Element)();
// Then
expect(resolvedElement).toBe(fallbackElement);
});
it("keeps the primary target when both targets exist", () => {
// Given
const primaryElement = document.createElement("div");
primaryElement.dataset.tourId = "view-first-scan-in-progress";
const fallbackElement = document.createElement("div");
fallbackElement.dataset.tourId = "view-first-scan-tabs";
document.body.append(primaryElement, fallbackElement);
const driveStep = adaptStep("view-first-scan", {
target: "in-progress",
fallbackTarget: "tabs",
title: "Your scan is running",
});
// When
const resolvedElement = (driveStep.element as () => Element)();
// Then
expect(resolvedElement).toBe(primaryElement);
});
it("still reports configuration drift when both targets are missing", () => {
// Given
const driveStep = adaptStep("view-first-scan", {
target: "in-progress",
fallbackTarget: "tabs",
title: "Your scan is running",
});
// When
const resolveElement = () => (driveStep.element as () => Element)();
// Then
expect(resolveElement).toThrow(
'Tour "view-first-scan" references missing selector: [data-tour-id="view-first-scan-in-progress"]',
);
});
});
@@ -72,6 +72,19 @@ describe("buildViewFirstScanTour with a running scan", () => {
expect(tour.version).toBe(viewFirstScanTour.version);
});
it("falls back to the stable tabs anchor if the running row disappears", () => {
// Given
const runningScanStep = tour.steps.find(
(step) => step.target === "in-progress",
);
// When
const fallbackTarget = runningScanStep?.fallbackTarget;
// Then
expect(fallbackTarget).toBe("tabs");
});
it("never targets an element outside the allowed anchor set", () => {
for (const target of definedTargets(tour)) {
expect(ALLOWED_TARGETS).toContain(target);
+3
View File
@@ -45,6 +45,9 @@ export interface TourCompletionRecord {
// Modal step omits `target`; anchored step provides the `data-tour-id` value (no brackets).
export interface TourStep<TTarget extends string = string> {
target?: TTarget;
// Optional stable anchor used when a volatile primary target disappears before
// driver.js resolves the step (for example, a running scan row that completes).
fallbackTarget?: TTarget;
title?: string;
description?: string;
side?: TourStepSide;
+6 -1
View File
@@ -172,11 +172,16 @@ export function adaptStep<TTarget extends string>(
if (step.target) {
const selector = toSelector(`${tourId}-${step.target}`);
const fallbackSelector = step.fallbackTarget
? toSelector(`${tourId}-${step.fallbackTarget}`)
: undefined;
driveStep.element = () => {
if (typeof document === "undefined") {
throw new Error("Tour element resolved without a DOM");
}
const found = document.querySelector(selector);
const found =
document.querySelector(selector) ??
(fallbackSelector ? document.querySelector(fallbackSelector) : null);
if (!found) {
throw new Error(
`Tour "${tourId}" references missing selector: ${selector}`,
+3 -3
View File
@@ -33,9 +33,8 @@ const INTRO_STEP = {
/**
* Builds the tour for the scans page. When a scan is already running we anchor the
* In Progress row first (and mention the other tabs in copy); otherwise we fall back
* to highlighting Launch Scan and the tabs. Gating on `hasInProgressScan` keeps the
* tour from anchoring to a missing row — the same guard pattern the findings tour
* uses for an empty table.
* to highlighting Launch Scan and the tabs. The volatile row step itself falls back
* to the stable tabs anchor if the scan completes while the tour is open.
*/
export function buildViewFirstScanTour(
hasInProgressScan: boolean,
@@ -49,6 +48,7 @@ export function buildViewFirstScanTour(
INTRO_STEP,
{
target: "in-progress",
fallbackTarget: "tabs",
side: TOUR_STEP_SIDES.BOTTOM,
align: TOUR_STEP_ALIGNMENTS.START,
title: "Your scan is running",
+27 -15
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env node
// Tour alignment check (syntactic). Extracts `data-tour-id` values from every
// `ui/lib/tours/*.tour.ts` and verifies a matching attribute exists under `ui/`,
// in either the JSX form (`data-tour-id="..."`) or the object-property form
// (`"data-tour-id": "..."`) used for dynamically-spread anchors. Two directions:
// - Tour → DOM: fails on any tour `target` with no matching attribute.
// Tour alignment check (syntactic). Extracts primary and fallback `data-tour-id`
// values from every `ui/lib/tours/*.tour.ts` and verifies a matching attribute
// exists under `ui/`, in either the JSX form (`data-tour-id="..."`) or the
// object-property form (`"data-tour-id": "..."`) used for dynamically-spread
// anchors. Two directions:
// - Tour → DOM: fails on any `target` or `fallbackTarget` without an attribute.
// - DOM → tour: warns on any `data-tour-id` not referenced by any tour
// (does not fail — staged anchors during multi-PR rollouts are OK).
// Complements the semantic `prowler-tour` skill for CI/local runs without
@@ -41,7 +42,18 @@ async function findTourFiles(dir) {
}
const TOUR_ID_PATTERN = /\bid\s*:\s*["']([a-z0-9-]+)["']/m;
const TARGET_PATTERN = /\btarget\s*:\s*["']([a-z0-9-]+)["']/g;
const TARGET_PATTERN =
/\b(?:target|fallbackTarget)\s*:\s*["']([a-z0-9-]+)["']/g;
/**
* Extracts every primary and fallback target declared by a tour.
*
* @param {string} source
* @returns {string[]}
*/
export function extractTourTargets(source) {
return Array.from(source.matchAll(TARGET_PATTERN), (match) => match[1]);
}
async function parseTour(filePath) {
const source = await readFile(filePath, "utf8");
@@ -53,10 +65,7 @@ async function parseTour(filePath) {
}
const tourId = idMatch[1];
const targets = [];
for (const match of source.matchAll(TARGET_PATTERN)) {
targets.push(match[1]);
}
const targets = extractTourTargets(source);
return {
file: relative(UI_DIR, filePath),
@@ -155,7 +164,7 @@ async function main() {
if (tourOrphans.length === 0) {
const referenced = tours.reduce((sum, t) => sum + t.selectors.length, 0);
console.log(
`✓ Tour alignment OK — ${tours.length} tour(s), ${referenced} anchored step(s).`,
`✓ Tour alignment OK — ${tours.length} tour(s), ${referenced} anchor reference(s).`,
);
return;
}
@@ -172,7 +181,10 @@ async function main() {
process.exit(1);
}
main().catch((err) => {
console.error(err.stack || err.message);
process.exit(1);
});
const entryPoint = process.argv[1];
if (entryPoint && resolve(entryPoint) === fileURLToPath(import.meta.url)) {
main().catch((err) => {
console.error(err.stack || err.message);
process.exit(1);
});
}
+37
View File
@@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import { extractTourTargets } from "./check-tour-alignment.mjs";
describe("tour target extraction", () => {
it("should include primary and fallback targets", () => {
// Given
const source = `
{
target: "volatile-row",
fallbackTarget: "stable-tabs",
}
`;
// When
const targets = extractTourTargets(source);
// Then
expect(targets).toEqual(["volatile-row", "stable-tabs"]);
});
it("should include a target used only as a fallback", () => {
// Given
const source = `
{
fallbackTarget: "fallback-only-anchor",
title: "Fallback-only step",
}
`;
// When
const targets = extractTourTargets(source);
// Then
expect(targets).toEqual(["fallback-only-anchor"]);
});
});