Software vulnerability automation modal (#3920)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Ability in UI to set a software vulnerability automation on Software page
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -422,7 +422,7 @@ const ManagePolicyPage = ({
|
||||
{!!teamId && teamPoliciesError && <TableDataError />}
|
||||
{!!teamId &&
|
||||
!teamPoliciesError &&
|
||||
(isLoadingTeamPolicies ? (
|
||||
(isLoadingTeamPolicies && isLoadingFailingPoliciesWebhook ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<PoliciesListWrapper
|
||||
|
||||
@@ -19,10 +19,6 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&__info-header {
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
&__btn-wrap {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
@@ -32,42 +28,6 @@
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
&__advanced-options-button {
|
||||
margin: $pad-medium 0;
|
||||
color: $core-vibrant-blue;
|
||||
font-weight: $bold;
|
||||
font-size: $x-small;
|
||||
}
|
||||
|
||||
.downcarat {
|
||||
&::after {
|
||||
content: url("../assets/images/icon-chevron-blue-16x16@2x.png");
|
||||
transform: scale(0.5);
|
||||
width: 16px;
|
||||
border-radius: 0px;
|
||||
padding: 0px;
|
||||
padding-left: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.upcarat {
|
||||
&::after {
|
||||
content: url("../assets/images/icon-chevron-blue-16x16@2x.png");
|
||||
transform: scale(0.5) rotate(180deg);
|
||||
width: 16px;
|
||||
border-radius: 0px;
|
||||
padding: 0px;
|
||||
padding-left: 2px;
|
||||
margin-bottom: 4px;
|
||||
margin-left: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.Select-value-label {
|
||||
font-size: $small;
|
||||
}
|
||||
|
||||
.tooltip__tooltip-icon {
|
||||
img {
|
||||
width: 16px;
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { InjectedRouter } from "react-router/lib/Router";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import { useDebouncedCallback } from "use-debounce/lib";
|
||||
import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { IConfig, IConfigNested } from "interfaces/config";
|
||||
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook";
|
||||
// @ts-ignore
|
||||
import { getConfig } from "redux/nodes/app/actions";
|
||||
// @ts-ignore
|
||||
import { renderFlash } from "redux/nodes/notifications/actions";
|
||||
import configAPI from "services/entities/config";
|
||||
import usersAPI, { IGetMeResponse } from "services/entities/users";
|
||||
import softwareAPI, {
|
||||
ISoftwareResponse,
|
||||
ISoftwareCountResponse,
|
||||
@@ -29,6 +38,7 @@ import ExternalLinkIcon from "../../../../assets/images/open-new-tab-12x12@2x.pn
|
||||
import QuestionIcon from "../../../../assets/images/icon-question-16x16@2x.png";
|
||||
|
||||
import generateTableHeaders from "./SoftwareTableConfig";
|
||||
import ManageAutomationsModal from "./components/ManageAutomationsModal";
|
||||
import EmptySoftware from "../components/EmptySoftware";
|
||||
|
||||
interface IManageSoftwarePageProps {
|
||||
@@ -49,7 +59,17 @@ const ManageSoftwarePage = ({
|
||||
router,
|
||||
location,
|
||||
}: IManageSoftwarePageProps): JSX.Element => {
|
||||
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<IConfigNested, Error, IWebhookSoftwareVulnerabilities>(
|
||||
["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 (
|
||||
<Button
|
||||
onClick={() => console.log("Manage automations button click")}
|
||||
onClick={onManageAutomationsClick}
|
||||
className={`${baseClass}__manage-automations button`}
|
||||
variant="brand"
|
||||
>
|
||||
@@ -200,8 +300,11 @@ const ManageSoftwarePage = ({
|
||||
const renderHeaderDescription = (state: ITeamsDropdownState) => {
|
||||
return (
|
||||
<p>
|
||||
Search for installed software and manage automations for detected
|
||||
vulnerabilities (CVEs) on{" "}
|
||||
Search for installed software{" "}
|
||||
{canAddOrRemoveSoftwareWebhook &&
|
||||
(!isPremiumTier || state.teamId === 0) &&
|
||||
"and manage automations for detected vulnerabilities (CVEs)"}{" "}
|
||||
on{" "}
|
||||
<b>
|
||||
{state.isPremiumTier && !!state.teamId
|
||||
? "all hosts assigned to this team"
|
||||
@@ -224,7 +327,7 @@ const ManageSoftwarePage = ({
|
||||
buttons={renderHeaderButtons}
|
||||
/>
|
||||
);
|
||||
}, [router, location]);
|
||||
}, [router, location, isLoadingSoftwareVulnerabilitiesWebhook]);
|
||||
|
||||
const renderSoftwareCount = useCallback(() => {
|
||||
const count = softwareCount;
|
||||
@@ -394,6 +497,24 @@ const ManageSoftwarePage = ({
|
||||
highlightOnHover
|
||||
/>
|
||||
)}
|
||||
|
||||
{showManageAutomationsModal && (
|
||||
<ManageAutomationsModal
|
||||
onCancel={toggleManageAutomationsModal}
|
||||
onCreateWebhookSubmit={onCreateWebhookSubmit}
|
||||
togglePreviewPayloadModal={togglePreviewPayloadModal}
|
||||
showPreviewPayloadModal={showPreviewPayloadModal}
|
||||
softwareVulnerabilityWebhookEnabled={
|
||||
softwareVulnerabilitiesWebhook &&
|
||||
softwareVulnerabilitiesWebhook.enable_vulnerabilities_webhook
|
||||
}
|
||||
currentDestinationUrl={
|
||||
(softwareVulnerabilitiesWebhook &&
|
||||
softwareVulnerabilitiesWebhook.destination_url) ||
|
||||
""
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
// @ts-ignore
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import IconToolTip from "components/IconToolTip";
|
||||
import validURL from "components/forms/validators/valid_url";
|
||||
|
||||
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook";
|
||||
import { useDeepEffect } from "utilities/hooks";
|
||||
import { size } from "lodash";
|
||||
|
||||
import PreviewPayloadModal from "../PreviewPayloadModal";
|
||||
|
||||
interface IManageAutomationsModalProps {
|
||||
onCancel: () => void;
|
||||
onCreateWebhookSubmit: (formData: IWebhookSoftwareVulnerabilities) => void;
|
||||
togglePreviewPayloadModal: () => void;
|
||||
showPreviewPayloadModal: boolean;
|
||||
softwareVulnerabilityWebhookEnabled?: boolean;
|
||||
currentDestinationUrl?: string;
|
||||
}
|
||||
|
||||
interface ICheckedSoftwareAutomation {
|
||||
name?: string;
|
||||
accessor: string;
|
||||
isChecked?: boolean;
|
||||
}
|
||||
|
||||
const useCheckboxListStateManagement = (
|
||||
softwareVulnerabilityWebhookEnabled = false
|
||||
) => {
|
||||
// Ability to add future software automations
|
||||
const availableSoftwareAutomations: ICheckedSoftwareAutomation[] = [
|
||||
{ accessor: "vulnerability", name: "Enable vulnerability automations" },
|
||||
];
|
||||
const currentSoftwareAutomations: ICheckedSoftwareAutomation[] = softwareVulnerabilityWebhookEnabled
|
||||
? availableSoftwareAutomations
|
||||
: [];
|
||||
|
||||
const [softwareAutomationsItems, setSoftwareAutomationsItems] = useState<
|
||||
ICheckedSoftwareAutomation[]
|
||||
>(() => {
|
||||
return (
|
||||
availableSoftwareAutomations &&
|
||||
availableSoftwareAutomations.map(
|
||||
(automation: ICheckedSoftwareAutomation) => {
|
||||
return {
|
||||
name: automation.name,
|
||||
accessor: automation.accessor,
|
||||
isChecked: currentSoftwareAutomations.some(
|
||||
(currentSoftwareAutomationItem: ICheckedSoftwareAutomation) =>
|
||||
currentSoftwareAutomationItem.accessor === automation.accessor
|
||||
),
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const updateSoftwareAutomationsItems = (
|
||||
softwareAutomationAccessor: string
|
||||
) => {
|
||||
setSoftwareAutomationsItems((prevState) => {
|
||||
const selectedSoftwareAutomation = softwareAutomationsItems.find(
|
||||
(softwareAutomationItem) =>
|
||||
softwareAutomationItem.accessor === softwareAutomationAccessor
|
||||
);
|
||||
|
||||
const updatedSoftwareAutomation = selectedSoftwareAutomation && {
|
||||
...selectedSoftwareAutomation,
|
||||
isChecked:
|
||||
!!selectedSoftwareAutomation && !selectedSoftwareAutomation.isChecked,
|
||||
};
|
||||
|
||||
// this is replacing the softwareAutomation object with the updatedSoftwareAutomation we just created
|
||||
const newState = prevState.map((currentSoftwareAutomation) => {
|
||||
return currentSoftwareAutomation.accessor ===
|
||||
softwareAutomationAccessor && updatedSoftwareAutomation
|
||||
? updatedSoftwareAutomation
|
||||
: currentSoftwareAutomation;
|
||||
});
|
||||
return newState;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
softwareAutomationsItems,
|
||||
updateSoftwareAutomationsItems,
|
||||
};
|
||||
};
|
||||
|
||||
const validateWebhookURL = (url: string) => {
|
||||
const errors: { [key: string]: string } = {};
|
||||
|
||||
if (!validURL(url)) {
|
||||
errors.url = "Please add a valid destination URL";
|
||||
}
|
||||
|
||||
const valid = !size(errors);
|
||||
return { valid, errors };
|
||||
};
|
||||
|
||||
const baseClass = "manage-automations-modal";
|
||||
|
||||
const ManageAutomationsModal = ({
|
||||
onCancel: onReturnToApp,
|
||||
onCreateWebhookSubmit,
|
||||
togglePreviewPayloadModal,
|
||||
showPreviewPayloadModal,
|
||||
softwareVulnerabilityWebhookEnabled,
|
||||
currentDestinationUrl,
|
||||
}: IManageAutomationsModalProps): JSX.Element => {
|
||||
const [destination_url, setDestinationUrl] = useState<string>(
|
||||
currentDestinationUrl || ""
|
||||
);
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||
|
||||
const {
|
||||
softwareAutomationsItems,
|
||||
updateSoftwareAutomationsItems,
|
||||
} = useCheckboxListStateManagement(softwareVulnerabilityWebhookEnabled);
|
||||
|
||||
useDeepEffect(() => {
|
||||
if (destination_url) {
|
||||
setErrors({});
|
||||
}
|
||||
}, [destination_url]);
|
||||
|
||||
const onURLChange = (value: string) => {
|
||||
setDestinationUrl(value);
|
||||
};
|
||||
|
||||
const handleSaveAutomation = (evt: React.MouseEvent<HTMLFormElement>) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { valid, errors: newErrors } = validateWebhookURL(destination_url);
|
||||
setErrors({
|
||||
...errors,
|
||||
...newErrors,
|
||||
});
|
||||
|
||||
if (valid) {
|
||||
// Ability to add future software automations
|
||||
const vulnerabilityWebhook = softwareAutomationsItems.find(
|
||||
(softwareAutomationItem) =>
|
||||
softwareAutomationItem.accessor === "vulnerability"
|
||||
);
|
||||
|
||||
onCreateWebhookSubmit({
|
||||
destination_url,
|
||||
enable_vulnerabilities_webhook: vulnerabilityWebhook?.isChecked,
|
||||
});
|
||||
|
||||
onReturnToApp();
|
||||
}
|
||||
};
|
||||
|
||||
if (showPreviewPayloadModal) {
|
||||
return <PreviewPayloadModal onCancel={togglePreviewPayloadModal} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onExit={onReturnToApp}
|
||||
title={"Manage automations"}
|
||||
className={baseClass}
|
||||
>
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__software-select-items`}>
|
||||
{softwareAutomationsItems &&
|
||||
softwareAutomationsItems.map(
|
||||
(softwareItem: ICheckedSoftwareAutomation) => {
|
||||
const { isChecked, name, accessor } = softwareItem;
|
||||
return (
|
||||
<div key={accessor} className={`${baseClass}__team-item`}>
|
||||
<Checkbox
|
||||
value={isChecked}
|
||||
name={name}
|
||||
onChange={() =>
|
||||
updateSoftwareAutomationsItems(softwareItem.accessor)
|
||||
}
|
||||
>
|
||||
{name}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="tooltip-wrap tooltip-wrap--input">
|
||||
<InputField
|
||||
inputWrapperClass={`${baseClass}__url-input`}
|
||||
name="webhook-url"
|
||||
label={"Destination URL"}
|
||||
type={"text"}
|
||||
value={destination_url}
|
||||
onChange={onURLChange}
|
||||
error={errors.url}
|
||||
hint={
|
||||
"For each new vulnerability detected, Fleet will send a JSON payload to this URL with a list of the affected hosts."
|
||||
}
|
||||
placeholder={"https://server.com/example"}
|
||||
/>
|
||||
<IconToolTip
|
||||
isHtml
|
||||
text={"<p>Provide a URL to deliver a<br />webhook request to.</p>"}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="text-link"
|
||||
onClick={togglePreviewPayloadModal}
|
||||
>
|
||||
Preview payload
|
||||
</Button>
|
||||
|
||||
<div className={`${baseClass}__button-wrap`}>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
onClick={onReturnToApp}
|
||||
variant="inverse"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
type="submit"
|
||||
variant="brand"
|
||||
onClick={handleSaveAutomation}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageAutomationsModal;
|
||||
+69
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./ManageAutomationsModal";
|
||||
+72
@@ -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 (
|
||||
<Modal title={"Example payload"} onExit={onCancel} className={baseClass}>
|
||||
<div className={`${baseClass}__preview-modal`}>
|
||||
<p>
|
||||
Want to learn more about how automations in Fleet work?{" "}
|
||||
<a
|
||||
href="https://fleetdm.com/docs/using-fleet/automations"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Check out the Fleet documentation
|
||||
<FleetIcon name="external-link" />
|
||||
</a>
|
||||
</p>
|
||||
<div className={`${baseClass}__payload-request-preview`}>
|
||||
<pre>POST https://server.com/example</pre>
|
||||
</div>
|
||||
<div className={`${baseClass}__payload-webhook-preview`}>
|
||||
<pre dangerouslySetInnerHTML={{ __html: syntaxHighlight(json) }} />
|
||||
</div>
|
||||
<div className={`${baseClass}__btn-wrap`}>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
onClick={onCancel}
|
||||
variant="brand"
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreviewPayloadModal;
|
||||
+38
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./PreviewPayloadModal";
|
||||
Reference in New Issue
Block a user