diff --git a/changes/issue-3762-software-vulnerability-webhook-modal b/changes/issue-3762-software-vulnerability-webhook-modal new file mode 100644 index 0000000000..857b0d4745 --- /dev/null +++ b/changes/issue-3762-software-vulnerability-webhook-modal @@ -0,0 +1 @@ +* Ability in UI to set a software vulnerability automation on Software page diff --git a/cypress/integration/free/admin.spec.ts b/cypress/integration/free/admin.spec.ts index b2dcc7baf4..f8afef1249 100644 --- a/cypress/integration/free/admin.spec.ts +++ b/cypress/integration/free/admin.spec.ts @@ -14,74 +14,86 @@ describe( cy.seedPolicies(); cy.addDockerHost(); }); - after(() => { cy.logout(); cy.stopDockerHost(); }); - describe("Mange hosts tests", () => { + describe("Dashboard and navigation", () => { + beforeEach(() => { + cy.loginWithCySession("anna@organization.com", "user123#"); + cy.visit("/dashboard"); + }); + it("displays intended admin dashboard", () => { + cy.getAttached(".homepage__wrapper").within(() => { + cy.findByText(/fleet test/i).should("exist"); + cy.getAttached(".hosts-summary").should("exist"); + cy.getAttached(".hosts-status").should("exist"); + cy.getAttached(".home-software").should("exist"); + cy.getAttached(".activity-feed").should("exist"); + }); + }); + it("displays intended admin top navigation", () => { + cy.getAttached(".site-nav-container").within(() => { + cy.findByText(/hosts/i).should("exist"); + cy.findByText(/software/i).should("exist"); + cy.findByText(/queries/i).should("exist"); + cy.findByText(/schedule/i).should("exist"); + cy.findByText(/policies/i).should("exist"); + cy.getAttached(".user-menu").click(); + cy.findByText(/settings/i).click(); + }); + cy.getAttached(".react-tabs__tab--selected").within(() => { + cy.findByText(/organization/i).should("exist"); + }); + cy.getAttached(".site-nav-container").within(() => { + cy.getAttached(".user-menu").click(); + cy.findByText(/manage users/i).click(); + }); + cy.getAttached(".react-tabs__tab--selected").within(() => { + cy.findByText(/users/i).should("exist"); + }); + }); + }); + describe("Manage hosts page", () => { beforeEach(() => { cy.loginWithCySession("anna@organization.com", "user123#"); cy.visit("/hosts/manage"); }); - - it("Can verify user is on the Manage Hosts page", () => { - cy.getAttached(".manage-hosts").within(() => { - cy.findByText(/edit columns/i).should("exist"); - }); - }); - - it("Can see correct global navigation items", () => { - cy.getAttached("nav").within(() => { - cy.findByText(/hosts/i).should("exist"); - cy.findByText(/queries/i).should("exist"); - cy.findByText(/schedule/i).should("exist"); - cy.findByText(/settings/i).should("exist"); - }); - }); - - it("Can verify teams is disabled", () => { + it("verifies teams is disabled on Manage Host page", () => { cy.contains(/team/i).should("not.exist"); }); - - it("Can see and click the 'Generate installer' button", () => { + it("allows admin to see and click the 'Generate installer' button", () => { cy.findByRole("button", { name: /generate installer/i }).click(); cy.contains(/team/i).should("not.exist"); cy.contains("button", /done/i).click(); }); - - it("Can manage and add enroll secret", () => { + it("allows admin to manage and add enroll secret", () => { cy.contains("button", /manage enroll secret/i).click(); cy.contains("button", /add secret/i).click(); cy.contains("button", /save/i).click(); cy.contains("button", /done/i).click(); }); - - it("Can open the 'Add label' form", () => { + it("allows admin to open the 'Add label' form", () => { cy.findByRole("button", { name: /add label/i }).click(); cy.findByRole("button", { name: /cancel/i }).click(); }); }); - describe("Host details tests", () => { beforeEach(() => { cy.loginWithCySession("anna@organization.com", "user123#"); cy.visit("/hosts/1"); }); - - it("Can verify teams is disabled", () => { + it("verifies teams is disabled on Host Details page", () => { cy.findByText(/team/i).should("not.exist"); cy.contains("button", /transfer/i).should("not.exist"); }); - - it("Can delete a query", () => { + it("allows admin to delete a query", () => { cy.findByRole("button", { name: /delete/i }).click(); cy.findByText(/delete host/i).should("exist"); cy.findByRole("button", { name: /cancel/i }).click(); }); - - it("Can create a new query", () => { + it("allows admin to create a new query", () => { cy.findByRole("button", { name: /query/i }).click(); cy.findByRole("button", { name: /create custom query/i }).should( "exist" @@ -91,18 +103,31 @@ describe( }); }); }); - - describe("Queries tests", () => { + describe("Manage software page", () => { + beforeEach(() => { + cy.loginWithCySession("anna@organization.com", "user123#"); + cy.visit("/software/manage"); + }); + it("allows global admin to click 'Manage automations' button", () => { + cy.getAttached(".manage-software-page__header-wrap").within(() => { + cy.findByRole("button", { name: /manage automations/i }).click(); + }); + cy.getAttached(".manage-automations-modal__button-wrap").within(() => { + cy.findByRole("button", { + name: /cancel/i, + }).click(); + }); + }); + }); + describe("Query pages", () => { beforeEach(() => { cy.loginWithCySession("anna@organization.com", "user123#"); cy.visit("/queries/manage"); }); - - it("Can see the 'Observer can run' column on the queries table", () => { + it("displays the 'Observer can run' column on the queries table", () => { cy.contains(/observer can run/i); }); - - it("Can add a new query", () => { + it("allows admin add a new query", () => { cy.findByRole("button", { name: /new query/i }).click(); cy.getAttached(".ace_text-input") .click({ force: true }) @@ -127,8 +152,7 @@ describe( }); cy.findByText(/query created/i).should("exist"); }); - - it("Can edit a query", () => { + it("allows admin to edit a query", () => { cy.findByText(/cypress test query/i).click({ force: true }); cy.getAttached(".ace_text-input") .click({ force: true }) @@ -139,8 +163,7 @@ describe( cy.findByText("Save").click(); // we have 'save as new' also cy.findByText(/query updated/i).should("exist"); }); - - it("Can run a query", () => { + it("allows admin to run a query", () => { cy.findByText(/cypress test query/i).click({ force: true }); cy.findByText(/run query/i).click({ force: true }); cy.findByText(/select targets/i).should("exist"); @@ -150,26 +173,22 @@ describe( cy.findByText(/querying selected hosts/i).should("exist"); // target count }); }); - - describe("Policies tests", () => { + describe("Manage policies page", () => { beforeEach(() => { cy.loginWithCySession("anna@organization.com", "user123#"); cy.visit("/policies/manage"); }); - - it("Can manage automations", () => { + it("allows admin to click 'Manage automations' button", () => { cy.findByRole("button", { name: /manage automations/i }).click(); cy.findByRole("button", { name: /cancel/i }).click(); }); - - it("Can add a policy", () => { + it("allows admin to add a new policy", () => { cy.findByRole("button", { name: /add a policy/i }).click(); cy.getAttached(".modal__ex").within(() => { cy.findByRole("button").click(); }); }); - - it("Can delete a policy", () => { + it("allows admin to delete a policy", () => { // select checkmark on table cy.getAttached("tbody").within(() => { cy.getAttached("tr") @@ -178,15 +197,13 @@ describe( cy.getAttached(".fleet-checkbox__input").check({ force: true }); }); }); - cy.findByRole("button", { name: /delete/i }).click(); cy.getAttached(".remove-policies-modal").within(() => { cy.findByRole("button", { name: /delete/i }).should("exist"); cy.findByRole("button", { name: /cancel/i }).click(); }); }); - - it("Can select a policy and verify user can run and save", () => { + it("allows admin to select a policy and see CTAs to run and save", () => { cy.getAttached(".data-table__table").within(() => { cy.findByRole("button", { name: /filevault enabled/i }).click(); }); @@ -196,58 +213,48 @@ describe( }); }); }); - - describe("Settings tests", () => { + describe("Admin settings page", () => { // cypress tends to fail on uncaught exceptions. since we have // our own error handling, it's suggested to use this block to // suppress so the tests will keep running Cypress.on("uncaught:exception", () => { return false; }); - beforeEach(() => { cy.loginWithCySession("anna@organization.com", "user123#"); cy.visit("/settings/users"); }); - - it("Can verify teams is disabled on the Settings page", () => { + it("hides access team settings", () => { cy.findByText(/teams/i).should("not.exist"); }); - - it("Can verify all other suboptions exist", () => { + it("allows admin to access other settings", () => { cy.getAttached(".react-tabs").within(() => { cy.findByText(/organization settings/i).should("exist"); cy.findByText(/users/i).click(); }); }); - - it("Can see and click the 'Create user' button", () => { + it("displays the 'Create user' button", () => { cy.findByRole("button", { name: /create user/i }).click(); }); - - it("Can verify teams is disabled for creating a user", () => { + it("hides assigning a user to a team", () => { cy.findByText(/team/i).should("not.exist"); }); - - it("Can verify user is not autorized to use the Team Settings page", () => { + it("verifies admin is not authorized to reach the Team Settings page", () => { cy.visit("/settings/teams"); cy.findByText(/you do not have permissions/i).should("exist"); }); }); - - describe("Profile tests", () => { + describe("User profile page", () => { beforeEach(() => { cy.loginWithCySession("anna@organization.com", "user123#"); cy.visit("/profile"); }); - - it("Can verify teams is disabled for the Profile page", () => { + it("verifies teams is disabled for the Profile page", () => { cy.getAttached(".user-settings__additional").within(() => { cy.findByText(/teams/i).should("not.exist"); }); }); - - it("Can verify the role of the user is admin", () => { + it("renders elements according to role-based access controls", () => { cy.getAttached(".user-settings__additional").within(() => { cy.findByText("Role").next().contains(/admin/i); }); diff --git a/cypress/integration/free/maintainer.spec.ts b/cypress/integration/free/maintainer.spec.ts index f9624146e1..104c46454a 100644 --- a/cypress/integration/free/maintainer.spec.ts +++ b/cypress/integration/free/maintainer.spec.ts @@ -14,63 +14,80 @@ describe( cy.seedPolicies(); cy.addDockerHost(); }); - after(() => { cy.logout(); cy.stopDockerHost(); }); - describe("Mange hosts tests", () => { + describe("Dashboard and navigation", () => { + beforeEach(() => { + cy.loginWithCySession("mary@organization.com", "user123#"); + cy.visit("/dashboard"); + }); + it("displays intended global maintainer dashboard", () => { + cy.getAttached(".homepage__wrapper").within(() => { + cy.findByText(/fleet test/i).should("exist"); + cy.getAttached(".hosts-summary").should("exist"); + cy.getAttached(".hosts-status").should("exist"); + cy.getAttached(".home-software").should("exist"); + cy.getAttached(".activity-feed").should("exist"); + }); + }); + it("displays intended global maintainer top navigation", () => { + cy.getAttached(".site-nav-container").within(() => { + cy.findByText(/hosts/i).should("exist"); + cy.findByText(/software/i).should("exist"); + cy.findByText(/queries/i).should("exist"); + cy.findByText(/schedule/i).should("exist"); + cy.findByText(/policies/i).should("exist"); + cy.getAttached(".user-menu").click(); + cy.findByText(/settings/i).should("not.exist"); + cy.findByText(/manage users/i).should("not.exist"); + }); + }); + }); + describe("Manage hosts page", () => { beforeEach(() => { cy.loginWithCySession("mary@organization.com", "user123#"); cy.visit("/hosts/manage"); }); - - it("Can verify user is on the Manage Hosts page", () => { + it("verifies maintainer is on the Manage Hosts page", () => { cy.getAttached(".manage-hosts").within(() => { cy.findByText(/edit columns/i).should("exist"); }); }); - - it("Can verify teams is disabled", () => { + it("verifies teams is disabled", () => { cy.contains(/team/i).should("not.exist"); }); - - it("Can see and click the 'Generate installer' button", () => { + it("allows maintainer to see and click the 'Generate installer' button", () => { cy.findByRole("button", { name: /generate installer/i }).click(); cy.contains(/team/i).should("not.exist"); cy.contains("button", /done/i).click(); }); - - it("Can manage the enroll secret", () => { + it("allows maintainer to manage the enroll secret", () => { cy.contains("button", /manage enroll secret/i).click(); cy.contains("button", /done/i).click(); }); - - it("Can open the 'Add label' form", () => { + it("allows maintainer to open the 'Add label' form", () => { cy.findByRole("button", { name: /add label/i }).click(); cy.findByRole("button", { name: /cancel/i }).click(); }); }); - describe("Host details tests", () => { beforeEach(() => { cy.loginWithCySession("mary@organization.com", "user123#"); cy.visit("/hosts/1"); }); - - it("Can verify teams is disabled", () => { + it("verifies teams is disabled", () => { cy.findByText(/team/i).should("not.exist"); cy.contains("button", /transfer/i).should("not.exist"); }); - - it("Can delete a query", () => { + it("allows maintainer to delete a host", () => { cy.findByRole("button", { name: /delete/i }).click(); cy.findByText(/delete host/i).should("exist"); cy.findByRole("button", { name: /cancel/i }).click(); }); - - it("Can create a new query", () => { + it("allows maintainer to create a new query on a host", () => { cy.findByRole("button", { name: /query/i }).click(); cy.findByRole("button", { name: /create custom query/i }).should( "exist" @@ -80,18 +97,45 @@ describe( }); }); }); - - describe("Queries tests", () => { + describe("Manage software page", () => { + beforeEach(() => { + cy.loginWithCySession("mary@organization.com", "user123#"); + cy.visit("/software/manage"); + }); + it("allows maintainer to click 'Manage automations' button", () => { + it("manages software automations when all teams selected", () => { + cy.getAttached(".manage-software-page__header-wrap").within(() => { + cy.getAttached(".Select").within(() => { + cy.findByText(/all teams/i).should("exist"); + }); + cy.findByRole("button", { name: /manage automations/i }).click(); + cy.findByRole("button", { name: /cancel/i }).click(); + }); + }); + it("hides manage automations button when all teams not selected", () => { + cy.getAttached(".manage-software-page__header-wrap").within(() => { + cy.getAttached(".Select").within(() => { + cy.getAttached(".Select-control").click(); + cy.getAttached(".Select-menu-outer").within(() => { + cy.findByText(/apples/i).should("exist"); + }); + cy.findByRole("button", { + name: /manage automations/i, + }).should("not.exist"); + }); + }); + }); + }); + }); + describe("Query pages", () => { beforeEach(() => { cy.loginWithCySession("mary@organization.com", "user123#"); cy.visit("/queries/manage"); }); - - it("Can see the 'Observer can run' column on the queries table", () => { + it("displays the 'Observer can run' column on the queries table", () => { cy.contains(/observer can run/i); }); - - it("Can add a new query", () => { + it("allows maintainer to add a new query", () => { cy.findByRole("button", { name: /new query/i }).click(); cy.getAttached(".ace_text-input") .click({ force: true }) @@ -108,7 +152,9 @@ describe( cy.findByLabelText(/description/i) .click() .type("Cypress test of create new query flow."); - cy.findByLabelText(/observers can run/i).click({ force: true }); + cy.findByLabelText(/observers can run/i).click({ + force: true, + }); cy.findByRole("button", { name: /save query/i }).click(); }); }); @@ -116,8 +162,7 @@ describe( }); cy.findByText(/query created/i).should("exist"); }); - - it("Can edit a query", () => { + it("allows maintainer to edit a query", () => { cy.findByText(/cypress test query/i).click({ force: true }); cy.getAttached(".ace_text-input") .click({ force: true }) @@ -128,8 +173,7 @@ describe( cy.findByText("Save").click(); // we have 'save as new' also cy.findByText(/query updated/i).should("exist"); }); - - it("Can run a query", () => { + it("allows maintainer to run a query", () => { cy.findByText(/cypress test query/i).click({ force: true }); cy.findByText(/run query/i).click({ force: true }); cy.findByText(/select targets/i).should("exist"); @@ -139,26 +183,22 @@ describe( cy.findByText(/querying selected hosts/i).should("exist"); // target count }); }); - - describe("Policies tests", () => { + describe("Manage policies page", () => { beforeEach(() => { cy.loginWithCySession("mary@organization.com", "user123#"); cy.visit("/policies/manage"); }); - - it("Can manage automations", () => { + it("allows maintainer to manage automations", () => { cy.findByRole("button", { name: /manage automations/i }).click(); cy.findByRole("button", { name: /cancel/i }).click(); }); - - it("Can add a policy", () => { + it("allows maintainer to add a policy", () => { cy.findByRole("button", { name: /add a policy/i }).click(); cy.getAttached(".modal__ex").within(() => { cy.findByRole("button").click(); }); }); - - it("Can delete a policy", () => { + it("allows maintainer to delete a policy", () => { // select checkmark on table cy.getAttached("tbody").within(() => { cy.getAttached("tr") @@ -167,21 +207,21 @@ describe( cy.getAttached(".fleet-checkbox__input").check({ force: true }); }); }); - cy.findByRole("button", { name: /delete/i }).click(); cy.getAttached(".remove-policies-modal").within(() => { cy.findByRole("button", { name: /delete/i }).should("exist"); cy.findByRole("button", { name: /cancel/i }).click(); }); }); - - it("Can select a policy and verify user can run and save", () => { + it("allows maintainer to select a policy and see CTAs to run and save", () => { cy.getAttached(".data-table__table").within(() => { cy.getAttached("tbody").within(() => { cy.getAttached("tr") .first() .within(() => { - cy.findByRole("button", { name: /filevault enabled/i }).click(); + cy.findByRole("button", { + name: /filevault enabled/i, + }).click(); }); }); }); @@ -191,14 +231,12 @@ describe( }); }); }); - - describe("Packs tests", () => { + describe("Manage packs page", () => { beforeEach(() => { cy.loginWithCySession("mary@organization.com", "user123#"); cy.visit("/packs/manage"); }); - - it("Can create a pack", () => { + it("allows maintainer to create a pack", () => { cy.findByRole("button", { name: /create new pack/i }).click(); cy.findByLabelText(/name/i).click().type("Errors and crashes"); cy.findByLabelText(/description/i) @@ -206,8 +244,7 @@ describe( .type("See all user errors and window crashes."); cy.findByRole("button", { name: /save query pack/i }).click(); }); - - it("Can delete a pack", () => { + it("allows maintainer to delete a pack", () => { // select checkmark on table cy.getAttached("tbody").within(() => { cy.getAttached("tr") @@ -218,31 +255,26 @@ describe( }); // cy.get(".fleet-checkbox__input").check({ force: true }); cy.findByRole("button", { name: /delete/i }).click(); - // Can't figure out how attach findByRole onto modal button // Can't use findByText because delete button under modal cy.get(".remove-pack-modal__btn-wrap > .button--alert") .contains("button", /delete/i) .click(); - cy.findByText(/successfully deleted/i).should("be.visible"); cy.findByText(/server errors/i).should("not.exist"); }); }); - - describe("Profile tests", () => { + describe("User profile page", () => { beforeEach(() => { cy.loginWithCySession("mary@organization.com", "user123#"); cy.visit("/profile"); }); - - it("Can verify teams is disabled for the Profile page", () => { + it("verifies teams is disabled for the Profile page", () => { cy.getAttached(".user-settings__additional").within(() => { cy.findByText(/teams/i).should("not.exist"); }); }); - - it("Can verify the role of the user is maintainer", () => { + it("renders elements according to role-based access controls", () => { cy.getAttached(".user-settings__additional").within(() => { cy.findByText("Role") .next() @@ -260,12 +292,10 @@ describe( Cypress.on("uncaught:exception", () => { return false; }); - beforeEach(() => { cy.loginWithCySession("mary@organization.com", "user123#"); }); - - it("Can verify user does not have access to settings", () => { + it("verifies maintainer does not have access to settings", () => { cy.findByText(/settings/i).should("not.exist"); cy.visit("/settings/organization"); cy.findByText(/you do not have permissions/i).should("exist"); diff --git a/cypress/integration/free/observer.spec.ts b/cypress/integration/free/observer.spec.ts index 0fda75fef2..0bae4a82e0 100644 --- a/cypress/integration/free/observer.spec.ts +++ b/cypress/integration/free/observer.spec.ts @@ -9,100 +9,103 @@ describe("Free tier - Observer user", () => { cy.seedPolicies(); cy.addDockerHost(); }); - after(() => { cy.logout(); cy.stopDockerHost(); }); - describe("Mange hosts tests", () => { + describe("Dashboard and navigation", () => { + beforeEach(() => { + cy.loginWithCySession("oliver@organization.com", "user123#"); + cy.visit("/dashboard"); + }); + it("displays intended global observer dashboard", () => { + cy.getAttached(".homepage__wrapper").within(() => { + cy.findByText(/fleet test/i).should("exist"); + cy.getAttached(".hosts-summary").should("exist"); + cy.getAttached(".hosts-status").should("exist"); + cy.getAttached(".home-software").should("exist"); + cy.getAttached(".activity-feed").should("exist"); + }); + }); + it("displays intended global observer top navigation", () => { + cy.getAttached(".site-nav-container").within(() => { + cy.findByText(/hosts/i).should("exist"); + cy.findByText(/software/i).should("exist"); + cy.findByText(/queries/i).should("exist"); + cy.findByText(/schedule/i).should("not.exist"); + cy.findByText(/policies/i).should("exist"); + cy.getAttached(".user-menu").click(); + cy.findByText(/settings/i).should("not.exist"); + cy.findByText(/manage users/i).should("not.exist"); + }); + }); + }); + describe("Manage hosts page", () => { beforeEach(() => { cy.loginWithCySession("oliver@organization.com", "user123#"); cy.visit("/hosts/manage"); }); - - it("Can verify nav is restricted", () => { - // we expect a 402 error from the teams API - // in Cypress, we can't update the context for if we're - // in the premium tier, so the tests runs the teams API - Cypress.on("uncaught:exception", () => { - return false; - }); - - // Nav restrictions - cy.findByText(/settings/i).should("not.exist"); - cy.findByText(/schedule/i).should("not.exist"); - cy.visit("/settings/organization"); - cy.findByText(/you do not have permissions/i).should("exist"); - cy.visit("/packs/manage"); - cy.findByText(/you do not have permissions/i).should("exist"); - cy.visit("/schedule/manage"); - cy.findByText(/you do not have permissions/i).should("exist"); - }); - - it("Can verify teams is disabled", () => { + it("verifies teams is disabled on Manage Host page", () => { cy.findByText(/teams/i).should("not.exist"); }); - - it("Can verify user cannot generate an installer", () => { + it("hides generate installer button", () => { cy.contains("button", /generate installer/i).should("not.exist"); }); - - it("Can verify user cannot add a label", () => { + it("hides add a label button", () => { cy.contains("button", /add label/i).should("not.exist"); }); - - it("Can verify user cannot manage the enroll secret", () => { + it("hides manage enroll secrets button", () => { cy.contains("button", /manage enroll secret/i).should("not.exist"); }); }); - - describe("Host details tests", () => { + describe("Host details page", () => { beforeEach(() => { cy.loginWithCySession("oliver@organization.com", "user123#"); cy.visit("/hosts/1"); }); - - it("Can verify teams is disabled", () => { + it("verifies teams is disabled on Host Details page", () => { cy.findByText(/team/i).should("not.exist"); }); - - it("Can verify user cannot transfer host", () => { + it("hides transfer host button", () => { cy.contains("button", /transfer/i).should("not.exist"); }); - - it("Can verify user cannot delete host", () => { + it("hides delete host button", () => { cy.contains("button", /delete/i).should("not.exist"); }); - - it("Can verify user cannot query host", () => { + it("hides query host button", () => { cy.contains("button", /query/i).click(); }); - - it("Can verify user cannot create query", () => { - cy.contains("button", /create custom query/i).should("not.exist"); + }); + describe("Manage software page", () => { + beforeEach(() => { + cy.loginWithCySession("oliver@organization.com", "user123#"); + cy.visit("/software/manage"); + }); + it("hides manage automations button", () => { + cy.getAttached(".manage-software-page__header-wrap").within(() => { + cy.findByRole("button", { name: /manage automations/i }).should( + "not.exist" + ); + }); }); }); - - describe("Queries tests", () => { + describe("Query page", () => { beforeEach(() => { cy.loginWithCySession("oliver@organization.com", "user123#"); cy.visit("/queries/manage"); }); - - it("Can verify that user does not see 'Observer can run' column", () => { + it("hides 'Observer can run' column", () => { cy.getAttached("thead").within(() => { cy.findByText(/observer can run/i).should("not.exist"); }); }); - - it("Can verify that user cannot create a query", () => { + it("hides create a query button", () => { cy.findByRole("button", { name: /create new query/i }).should( "not.exist" ); }); - - it("Can verify that user can select a query and only run it", () => { + it("verifies observer can select a query and only run it", () => { cy.getAttached(".data-table__table").within(() => { cy.findByRole("button", { name: /detect presence/i }).click(); }); @@ -115,53 +118,49 @@ describe("Free tier - Observer user", () => { cy.findByRole("button", { name: /run query/i }).should("exist"); }); }); - - describe("Policies tests", () => { + describe("Manage policies page", () => { beforeEach(() => { cy.loginWithCySession("oliver@organization.com", "user123#"); cy.visit("/policies/manage"); }); - - it("Can verify user cannot manage automations", () => { + it("hides manage automations button", () => { cy.findByRole("button", { name: /manage automations/i }).should( "not.exist" ); }); - - it("Can verify user cannot add a policy", () => { + it("hides add a policy button", () => { cy.findByRole("button", { name: /add a policy/i }).should("not.exist"); }); - - it("Can verify user cannot run, edit, or delete a policy", () => { + it("hides run, edit, or delete a policy", () => { cy.getAttached("tbody").within(() => { - cy.get("tr") + cy.getAttached("tr") .first() .within(() => { cy.get(".fleet-checkbox__input").should("not.exist"); }); - cy.findByText(/filevault enabled/i).click(); }); - + cy.getAttached(".data-table__table").within(() => { + cy.findByRole("button", { + name: /filevault enabled/i, + }).click(); + }); cy.getAttached(".policy-form__wrapper").within(() => { cy.findByRole("button", { name: /run/i }).should("not.exist"); cy.findByRole("button", { name: /save/i }).should("not.exist"); }); }); }); - - describe("Profile tests", () => { + describe("User profile page", () => { beforeEach(() => { cy.loginWithCySession("oliver@organization.com", "user123#"); cy.visit("/profile"); }); - - it("Can verify teams is disabled for the Profile page", () => { + it("verifies teams is disabled for the Profile page", () => { cy.getAttached(".user-settings__additional").within(() => { cy.findByText(/teams/i).should("not.exist"); }); }); - - it("Can verify the role of the user is observer", () => { + it("renders elements according to role-based access controls", () => { cy.getAttached(".user-settings__additional").within(() => { cy.findByText("Role") .next() @@ -179,11 +178,9 @@ describe("Free tier - Observer user", () => { Cypress.on("uncaught:exception", () => { return false; }); - beforeEach(() => { cy.loginWithCySession("oliver@organization.com", "user123#"); }); - it("should restrict navigation according to role-based access controls", () => { cy.findByText(/settings/i).should("not.exist"); cy.findByText(/schedule/i).should("not.exist"); diff --git a/cypress/integration/premium/admin.spec.ts b/cypress/integration/premium/admin.spec.ts index 7342a0b692..48b11a202c 100644 --- a/cypress/integration/premium/admin.spec.ts +++ b/cypress/integration/premium/admin.spec.ts @@ -120,14 +120,17 @@ describe("Premium tier - Admin user", () => { }); describe("Manage software page", () => { beforeEach(() => cy.visit("/software/manage")); - it("displays manage automations button when all teams selected", () => { + it("allows global admin to click 'Manage automations' button", () => { cy.getAttached(".manage-software-page__header-wrap").within(() => { cy.getAttached(".Select").within(() => { cy.findByText(/all teams/i).should("exist"); }); - cy.findByRole("button", { name: /manage automations/i }).should( - "exist" - ); + cy.findByRole("button", { name: /manage automations/i }).click(); + }); + cy.getAttached(".manage-automations-modal__button-wrap").within(() => { + cy.findByRole("button", { + name: /cancel/i, + }).click(); }); }); it("hides manage automations button when all teams not selected", () => { diff --git a/cypress/integration/premium/maintainer.spec.ts b/cypress/integration/premium/maintainer.spec.ts index 6c2beeb537..c2810fac9d 100644 --- a/cypress/integration/premium/maintainer.spec.ts +++ b/cypress/integration/premium/maintainer.spec.ts @@ -112,23 +112,27 @@ describe("Premium tier - Maintainer user", () => { }); describe("Manage software page", () => { beforeEach(() => cy.visit("/software/manage")); - it("displays manage automations button", () => { - cy.getAttached(".manage-software-page__header-wrap").within(() => { - cy.findByRole("button", { name: /manage automations/i }).should( - "exist" - ); + it("allows global maintainer to click 'Manage automations' button", () => { + it("manages software automations when all teams selected", () => { + cy.getAttached(".manage-software-page__header-wrap").within(() => { + cy.getAttached(".Select").within(() => { + cy.findByText(/all teams/i).should("exist"); + }); + cy.findByRole("button", { name: /manage automations/i }).click(); + cy.findByRole("button", { name: /cancel/i }).click(); + }); }); - }); - it("hides manage automations button when all teams not selected", () => { - cy.getAttached(".manage-software-page__header-wrap").within(() => { - cy.getAttached(".Select").within(() => { - cy.getAttached(".Select-control").click(); - cy.getAttached(".Select-menu-outer").within(() => { - cy.findByText(/apples/i).should("exist"); + it("hides manage automations button when all teams not selected", () => { + cy.getAttached(".manage-software-page__header-wrap").within(() => { + cy.getAttached(".Select").within(() => { + cy.getAttached(".Select-control").click(); + cy.getAttached(".Select-menu-outer").within(() => { + cy.findByText(/apples/i).should("exist"); + }); + cy.findByRole("button", { + name: /manage automations/i, + }).should("not.exist"); }); - cy.findByRole("button", { - name: /manage automations/i, - }).should("not.exist"); }); }); }); diff --git a/cypress/integration/premium/observer.spec.ts b/cypress/integration/premium/observer.spec.ts index 9252e44dcc..e69d112526 100644 --- a/cypress/integration/premium/observer.spec.ts +++ b/cypress/integration/premium/observer.spec.ts @@ -199,9 +199,13 @@ describe("Premium tier - Observer user", () => { .first() .within(() => { cy.contains(".fleet-checkbox__input").should("not.exist"); - cy.findByText(/filevault enabled/i).click(); }); }); + cy.getAttached(".data-table__table").within(() => { + cy.findByRole("button", { + name: /filevault enabled/i, + }).click(); + }); cy.getAttached(".policy-form__wrapper").within(() => { cy.findByRole("button", { name: /run/i }).should("not.exist"); cy.findByRole("button", { name: /save/i }).should("not.exist"); diff --git a/frontend/interfaces/config.ts b/frontend/interfaces/config.ts index a43bd63589..8fdc5910f0 100644 --- a/frontend/interfaces/config.ts +++ b/frontend/interfaces/config.ts @@ -1,6 +1,10 @@ /* Config interface is a flattened version of the fleet/config API response */ -import { IWebhookFailingPolicies } from "interfaces/webhook"; +import { + IWebhookHostStatus, + IWebhookFailingPolicies, + IWebhookSoftwareVulnerabilities, +} from "interfaces/webhook"; import PropTypes from "prop-types"; export default PropTypes.shape({ @@ -226,13 +230,9 @@ export interface IConfigNested { databases_path: string; }; webhook_settings: { - host_status_webhook: { - enable_host_status_webhook: boolean; - destination_url: string; - host_percentage: number; - days_count: number; - }; + host_status_webhook: IWebhookHostStatus; failing_policies_webhook: IWebhookFailingPolicies; + vulnerabilities_webhook: IWebhookSoftwareVulnerabilities; }; logging: { debug: boolean; diff --git a/frontend/interfaces/webhook.ts b/frontend/interfaces/webhook.ts index d0fe0caf49..ff4de0dc1e 100644 --- a/frontend/interfaces/webhook.ts +++ b/frontend/interfaces/webhook.ts @@ -7,9 +7,21 @@ export default PropTypes.shape({ host_batch_size: PropTypes.number, }); +export interface IWebhookHostStatus { + enable_host_status_webhook?: boolean; + destination_url?: string; + host_percentage?: number; + days_count?: number; +} export interface IWebhookFailingPolicies { destination_url?: string; policy_ids?: number[]; enable_failing_policies_webhook?: boolean; host_batch_size?: number; } + +export interface IWebhookSoftwareVulnerabilities { + destination_url?: string; + enable_vulnerabilities_webhook?: boolean; + host_batch_size?: number; +} diff --git a/frontend/pages/admin/AppSettingsPage/AppSettingsPage.tsx b/frontend/pages/admin/AppSettingsPage/AppSettingsPage.tsx index 697d209e64..2520a2c8f2 100644 --- a/frontend/pages/admin/AppSettingsPage/AppSettingsPage.tsx +++ b/frontend/pages/admin/AppSettingsPage/AppSettingsPage.tsx @@ -26,7 +26,7 @@ export const baseClass = "app-settings"; const AppSettingsPage = (): JSX.Element => { const dispatch = useDispatch(); - const { config, setConfig } = useContext(AppContext); + const { setConfig } = useContext(AppContext); const { data: appConfig, diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx index a3b455d459..22f17b08a2 100644 --- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx @@ -422,7 +422,7 @@ const ManagePolicyPage = ({ {!!teamId && teamPoliciesError && } {!!teamId && !teamPoliciesError && - (isLoadingTeamPolicies ? ( + (isLoadingTeamPolicies && isLoadingFailingPoliciesWebhook ? ( ) : ( { - const { availableTeams, currentTeam } = useContext(AppContext); + const dispatch = useDispatch(); + const { + availableTeams, + currentTeam, + setAvailableTeams, + setCurrentUser, + setConfig, + isPremiumTier, + isGlobalAdmin, + isGlobalMaintainer, + } = useContext(AppContext); const [isLoadingSoftware, setIsLoadingSoftware] = useState(true); const [isLoadingCount, setIsLoadingCount] = useState(true); @@ -62,6 +82,10 @@ const ManageSoftwarePage = ({ >(DEFAULT_SORT_DIRECTION); const [sortHeader, setSortHeader] = useState(DEFAULT_SORT_HEADER); const [pageIndex, setPageIndex] = useState(0); + const [showManageAutomationsModal, setShowManageAutomationsModal] = useState( + false + ); + const [showPreviewPayloadModal, setShowPreviewPayloadModal] = useState(false); // TODO: experiment to see if we need this state and effect or can we rely solely on the router/location for the dropdown state? useEffect(() => { @@ -70,6 +94,14 @@ const ManageSoftwarePage = ({ // TODO: combine string and object so only one array element in query key; figure out typing and // destructuring for queryfn + + useQuery(["me"], () => usersAPI.me(), { + onSuccess: ({ user, available_teams }: IGetMeResponse) => { + setCurrentUser(user); + setAvailableTeams(available_teams); + }, + }); + const { data: software, error: softwareError } = useQuery< ISoftwareResponse, Error @@ -153,6 +185,22 @@ const ManageSoftwarePage = ({ } ); + const canAddOrRemoveSoftwareWebhook = isGlobalAdmin || isGlobalMaintainer; + + const { + data: softwareVulnerabilitiesWebhook, + isLoading: isLoadingSoftwareVulnerabilitiesWebhook, + refetch: refetchSoftwareVulnerabilitiesWebhook, + } = useQuery( + ["config"], + () => configAPI.loadAll(), + { + enabled: canAddOrRemoveSoftwareWebhook, + select: (data: IConfigNested) => + data.webhook_settings.vulnerabilities_webhook, + } + ); + const onQueryChange = useDebouncedCallback( async ({ pageIndex: newPageIndex, @@ -173,6 +221,57 @@ const ManageSoftwarePage = ({ 300 ); + const toggleManageAutomationsModal = () => + setShowManageAutomationsModal(!showManageAutomationsModal); + + const togglePreviewPayloadModal = useCallback(() => { + setShowPreviewPayloadModal(!showPreviewPayloadModal); + }, [setShowPreviewPayloadModal, showPreviewPayloadModal]); + + const onManageAutomationsClick = () => { + toggleManageAutomationsModal(); + }; + + const onCreateWebhookSubmit = async ({ + destination_url, + enable_vulnerabilities_webhook, + }: IWebhookSoftwareVulnerabilities) => { + try { + const request = configAPI.update({ + webhook_settings: { + vulnerabilities_webhook: { + destination_url, + enable_vulnerabilities_webhook, + }, + }, + }); + await request.then(() => { + dispatch( + renderFlash( + "success", + "Successfully updated vulnerability automations." + ) + ); + }); + } catch { + dispatch( + renderFlash( + "error", + "Could not update vulnerability automations. Please try again." + ) + ); + } finally { + toggleManageAutomationsModal(); + refetchSoftwareVulnerabilitiesWebhook(); + // Config must be updated in both Redux and AppContext + dispatch(getConfig()) + .then((configState: IConfig) => { + setConfig(configState); + }) + .catch(() => false); + } + }; + const onTeamSelect = () => { setPageIndex(0); }; @@ -181,12 +280,13 @@ const ManageSoftwarePage = ({ state: ITeamsDropdownState ): JSX.Element | null => { if ( - (state.isGlobalAdmin || state.isGlobalMaintainer) && - state.teamId === 0 + canAddOrRemoveSoftwareWebhook && + (!isPremiumTier || state.teamId === 0) && + !isLoadingSoftwareVulnerabilitiesWebhook ) { return ( + +
+ + +
+ + + ); +}; + +export default ManageAutomationsModal; diff --git a/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/_styles.scss b/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/_styles.scss new file mode 100644 index 0000000000..6cff1e6724 --- /dev/null +++ b/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/_styles.scss @@ -0,0 +1,69 @@ +.manage-automations-modal { + width: 778px; + + &__button-wrap { + display: flex; + justify-content: flex-end; + margin: $pad-large 0 0; + } + + .button--inverse { + padding: $pad-small; + margin-right: $pad-small; + } + + pre, + code { + background-color: $ui-off-white; + color: $core-fleet-blue; + border: 1px solid $ui-fleet-blue-15; + border-radius: 4px; + padding: 7px $pad-medium; + margin: $pad-large 0 0 44px; + } + + &__error { + color: $ui-error; + } + + .tooltip-wrap { + display: flex; + align-items: center; + width: 100%; + margin-bottom: $pad-large; + + .icon-tooltip { + span { + display: flex; + align-items: center; + } + p { + text-align: center; + } + } + + .form-field--input { + display: flex; + flex-direction: column; + width: 100%; + margin-right: $pad-small; + margin-bottom: 0; + } + + .form-field__hint { + font-style: normal; + } + } + + .tooltip-wrap--input { + .icon-tooltip { + span { + margin-top: -5px; + } + } + } + + .form-field__label--error { + color: $ui-error; + } +} diff --git a/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/index.ts b/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/index.ts new file mode 100644 index 0000000000..c9128e3c2d --- /dev/null +++ b/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/index.ts @@ -0,0 +1 @@ +export { default } from "./ManageAutomationsModal"; diff --git a/frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/PreviewPayloadModal.tsx b/frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/PreviewPayloadModal.tsx new file mode 100644 index 0000000000..10b70d40b9 --- /dev/null +++ b/frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/PreviewPayloadModal.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import { syntaxHighlight } from "fleet/helpers"; + +import Modal from "components/Modal"; +import Button from "components/buttons/Button"; +// @ts-ignore +import FleetIcon from "components/icons/FleetIcon"; + +const baseClass = "preview-data-modal"; + +interface IPreviewPayloadModalProps { + onCancel: () => void; +} + +const PreviewPayloadModal = ({ + onCancel, +}: IPreviewPayloadModalProps): JSX.Element => { + const json = { + timestamp: "0000-00-00T00:00:00Z", + vulnerability: { + cve: "CVE-2014-9471", + details_link: "https://nvd.nist.gov/vuln/detail/CVE-2014-9471", + hosts_affected: [ + { + id: 1, + hostname: "macbook-1", + url: "https://fleet.example.com/hosts/1", + }, + { + id: 2, + hostname: "macbook-2", + url: "https://fleet.example.com/hosts/2", + }, + ], + }, + }; + + return ( + +
+

+ Want to learn more about how automations in Fleet work?{" "} + + Check out the Fleet documentation  + + +

+
+
POST https://server.com/example
+
+
+
+        
+
+ +
+
+
+ ); +}; + +export default PreviewPayloadModal; diff --git a/frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/_styles.scss b/frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/_styles.scss new file mode 100644 index 0000000000..b136056481 --- /dev/null +++ b/frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/_styles.scss @@ -0,0 +1,38 @@ +.preview-payload-modal { + &__sandbox-info { + margin-top: $pad-medium; + + p { + margin: 0; + margin-bottom: $pad-medium; + } + + p:last-child { + margin-bottom: 0; + } + } + + a { + color: $core-vibrant-blue; + font-weight: $bold; + font-size: $x-small; + text-decoration: none; + } + + &__btn-wrap { + display: flex; + flex-direction: row-reverse; + } + + &__btn { + margin-left: 12px; + } + + .tooltip__tooltip-icon { + img { + width: 16px; + height: 16px; + vertical-align: top; + } + } +} diff --git a/frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/index.ts b/frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/index.ts new file mode 100644 index 0000000000..bc08b4723d --- /dev/null +++ b/frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/index.ts @@ -0,0 +1 @@ +export { default } from "./PreviewPayloadModal";