@cryptotaxi247 / CoPilot / commits / ffb9b35f

Mssp check (#366)

* Add MSSP license check before customer creation * Fix case sensitivity in MSSP license feature check * Remove redundant return statement in create_customer function * Improve error message for missing license in get_license function * Update MSSP license check to enforce customer provisioning limits and add custom error messages * Refactor MSSP license check to clarify customer provisioning limits and improve error handling for license validation * Update MSSP license check to handle exceeding customer limits with feature enablement instead of raising an exception * chore: update dependencies in frontend * refactor: add shared license check * feat: add mssp license check * feat: add logging for license check during customer creation * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Dec 10, 2024 at 08:54 UTC ffb9b35f9c13936fd53dba0285ae661282aee1da
15 files changed +320 -99
.vscode/settings.json
+1
@@ -39,6 +39,7 @@
39 "Logsource",
40 "majesticons",
41 "mimecast",
42 + "MSSP",
43 "mynaui",
44 "nightwatch",
45 "ntime",
backend/app/customers/routes/customers.py
+53
@@ -27,6 +27,7 @@ from app.healthchecks.agents.schema.agents import AgentHealthCheckResponse
27 from app.healthchecks.agents.schema.agents import TimeCriteriaModel
28 from app.healthchecks.agents.services.agents import velociraptor_agents_healthcheck
29 from app.healthchecks.agents.services.agents import wazuh_agents_healthcheck
30 +from app.middleware.license import is_feature_enabled
31
32 customers_router = APIRouter()
33
@@ -72,6 +73,57 @@ async def verify_unique_customer_code(
73 )
74
75
76 +async def mssp_license_check(session: AsyncSession):
77 + """
78 + Check if the current number of provisioned customers is within the allowed range based on the MSSP license type.
79 + Customer 0 is free, 1-5 customers require an "MSSP 1-5" license, and 6-10 customers require an "MSSP 6-10" license.
80 +
81 + Args:
82 + session (AsyncSession): The database session.
83 +
84 + Raises:
85 + HTTPException: If the MSSP is not allowed to provision more customers.
86 + """
87 + # Select all customers to check the number of provisioned customers
88 + stmt = select(Customers)
89 + result = await session.execute(stmt)
90 + customers = result.scalars().all()
91 + provisioned_customers = len(customers)
92 + logger.info(f"Provisioned customers: {provisioned_customers}")
93 +
94 + if 1 <= provisioned_customers <= 5:
95 + # Check the license of the MSSP if the number of provisioned customers is between 1 and 5
96 + try:
97 + await is_feature_enabled(
98 + "MSSP 5",
99 + session,
100 + message="You have reached the maximum number of customers allowed for your license type. Please upgrade your license to provision more customers.",
101 + )
102 + except HTTPException as e:
103 + if e.status_code == 400:
104 + # If MSSP 1-5 license check fails, check for MSSP 6-10 license
105 + await is_feature_enabled(
106 + "MSSP 10",
107 + session,
108 + message="You have reached the maximum number of customers allowed for your license type. Please upgrade your license to provision more customers.",
109 + )
110 + else:
111 + raise e
112 + elif 6 <= provisioned_customers <= 10:
113 + # Check the license of the MSSP if the number of provisioned customers is between 6 and 10
114 + await is_feature_enabled(
115 + "MSSP 10",
116 + session,
117 + message="You have reached the maximum number of customers allowed for your license type. Please upgrade your license to provision more customers.",
118 + )
119 + elif provisioned_customers > 10:
120 + await is_feature_enabled(
121 + "MSSP Unlimited",
122 + session,
123 + message="You have reached the maximum number of customers allowed for your license type. Please upgrade your license to provision more customers.",
124 + )
125 +
126 +
127 @customers_router.post(
128 "",
129 response_model=CustomerResponse,
@@ -95,6 +147,7 @@ async def create_customer(
147 Raises:
148 None
149 """
150 + await mssp_license_check(session)
151 await verify_unique_customer_code(session, customer)
152 logger.info(f"Creating new customer: {customer}")
153 new_customer = Customers(**customer.dict())
backend/app/middleware/license.py
+5 -2
@@ -372,7 +372,7 @@ async def get_license(session: AsyncSession) -> License:
372 result = await session.execute(select(License))
373 license = result.scalars().first()
374 if license is None:
375 - raise HTTPException(status_code=404, detail="No license found")
375 + raise HTTPException(status_code=404, detail="No license found. A license must be created first.")
376 else:
377 return license
378
@@ -392,7 +392,7 @@ def is_license_expired(license: dict) -> bool:
392 return dt.now() > expires
393
394
395 -async def is_feature_enabled(feature_name: str, session: AsyncSession) -> bool:
395 +async def is_feature_enabled(feature_name: str, session: AsyncSession, message: str = None) -> bool:
396 """
397 Check if a feature is enabled in a license.
398
@@ -410,6 +410,9 @@ async def is_feature_enabled(feature_name: str, session: AsyncSession) -> bool:
410 if data_object["name"] == feature_name and data_object["intValue"] == 1:
411 return True
412
413 + if message:
414 + raise HTTPException(status_code=400, detail=message)
415 +
416 raise HTTPException(status_code=400, detail="Feature not enabled. You must purchase a license to use this feature.")
417
418
frontend/package-lock.json
+56 -58
@@ -13,7 +13,7 @@
13 "@fontsource/jetbrains-mono": "^5.1.1",
14 "@fontsource/lexend": "^5.1.1",
15 "@fontsource/public-sans": "^5.1.1",
16 - "@shikijs/markdown-it": "^1.24.0",
16 + "@shikijs/markdown-it": "^1.24.2",
17 "@tailwindcss/container-queries": "^0.1.1",
18 "@vueuse/core": "^12.0.0",
19 "axios": "^1.7.9",
@@ -34,7 +34,7 @@
34 "pinia": "^2.3.0",
35 "pinia-plugin-persistedstate": "^4.1.3",
36 "secure-ls": "^2.0.0",
37 - "shiki": "^1.24.0",
37 + "shiki": "^1.24.2",
38 "validator": "^13.12.0",
39 "vue": "^3.5.13",
40 "vue-advanced-cropper": "^2.8.9",
@@ -49,7 +49,7 @@
49 "devDependencies": {
50 "@antfu/eslint-config": "^3.11.2",
51 "@clack/prompts": "^0.8.2",
52 - "@iconify/vue": "^4.1.2",
52 + "@iconify/vue": "^4.2.0",
53 "@tsconfig/node20": "^20.1.4",
54 "@types/bytes": "^3.1.5",
55 "@types/file-saver": "^2.0.7",
@@ -92,7 +92,7 @@
92 "node": ">=18.0.0"
93 },
94 "optionalDependencies": {
95 - "@rollup/rollup-linux-x64-gnu": "^4.28.0"
95 + "@rollup/rollup-linux-x64-gnu": "^4.28.1"
96 }
97 },
98 "node_modules/@ajoelp/json-to-formdata": {
@@ -1668,9 +1668,9 @@
1668 "license": "MIT"
1669 },
1670 "node_modules/@iconify/vue": {
1671 - "version": "4.1.2",
1672 - "resolved": "https://registry.npmjs.org/@iconify/vue/-/vue-4.1.2.tgz",
1673 - "integrity": "sha512-CQnYqLiQD5LOAaXhBrmj1mdL2/NCJvwcC4jtW2Z8ukhThiFkLDkutarTOV2trfc9EXqUqRs0KqXOL9pZ/IyysA==",
1671 + "version": "4.2.0",
1672 + "resolved": "https://registry.npmjs.org/@iconify/vue/-/vue-4.2.0.tgz",
1673 + "integrity": "sha512-CMynoz9BDWugDO2B7LU/s8L99dHCiqDGCjCki6bhVx5etZhw9x0BTV7wWRdj82jtl1yQTc+QQRcHQmSvUY6R+g==",
1674 "dev": true,
1675 "license": "MIT",
1676 "dependencies": {
@@ -2516,9 +2516,9 @@
2516 ]
2517 },
2518 "node_modules/@rollup/rollup-linux-x64-gnu": {
2519 - "version": "4.28.0",
2520 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.0.tgz",
2521 - "integrity": "sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw==",
2519 + "version": "4.28.1",
2520 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz",
2521 + "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==",
2522 "cpu": [
2523 "x64"
2524 ],
@@ -2592,54 +2592,54 @@
2592 "license": "MIT"
2593 },
2594 "node_modules/@shikijs/core": {
2595 - "version": "1.24.0",
2596 - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.24.0.tgz",
2597 - "integrity": "sha512-6pvdH0KoahMzr6689yh0QJ3rCgF4j1XsXRHNEeEN6M4xJTfQ6QPWrmHzIddotg+xPJUPEPzYzYCKzpYyhTI6Gw==",
2595 + "version": "1.24.2",
2596 + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.24.2.tgz",
2597 + "integrity": "sha512-BpbNUSKIwbKrRRA+BQj0BEWSw+8kOPKDJevWeSE/xIqGX7K0xrCZQ9kK0nnEQyrzsUoka1l81ZtJ2mGaCA32HQ==",
2598 "license": "MIT",
2599 "dependencies": {
2600 - "@shikijs/engine-javascript": "1.24.0",
2601 - "@shikijs/engine-oniguruma": "1.24.0",
2602 - "@shikijs/types": "1.24.0",
2600 + "@shikijs/engine-javascript": "1.24.2",
2601 + "@shikijs/engine-oniguruma": "1.24.2",
2602 + "@shikijs/types": "1.24.2",
2603 "@shikijs/vscode-textmate": "^9.3.0",
2604 "@types/hast": "^3.0.4",
2605 "hast-util-to-html": "^9.0.3"
2606 }
2607 },
2608 "node_modules/@shikijs/engine-javascript": {
2609 - "version": "1.24.0",
2610 - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.24.0.tgz",
2611 - "integrity": "sha512-ZA6sCeSsF3Mnlxxr+4wGEJ9Tto4RHmfIS7ox8KIAbH0MTVUkw3roHPHZN+LlJMOHJJOVupe6tvuAzRpN8qK1vA==",
2609 + "version": "1.24.2",
2610 + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.24.2.tgz",
2611 + "integrity": "sha512-EqsmYBJdLEwEiO4H+oExz34a5GhhnVp+jH9Q/XjPjmBPc6TE/x4/gD0X3i0EbkKKNqXYHHJTJUpOLRQNkEzS9Q==",
2612 "license": "MIT",
2613 "dependencies": {
2614 - "@shikijs/types": "1.24.0",
2614 + "@shikijs/types": "1.24.2",
2615 "@shikijs/vscode-textmate": "^9.3.0",
2616 "oniguruma-to-es": "0.7.0"
2617 }
2618 },
2619 "node_modules/@shikijs/engine-oniguruma": {
2620 - "version": "1.24.0",
2621 - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.24.0.tgz",
2622 - "integrity": "sha512-Eua0qNOL73Y82lGA4GF5P+G2+VXX9XnuUxkiUuwcxQPH4wom+tE39kZpBFXfUuwNYxHSkrSxpB1p4kyRW0moSg==",
2620 + "version": "1.24.2",
2621 + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.24.2.tgz",
2622 + "integrity": "sha512-ZN6k//aDNWRJs1uKB12pturKHh7GejKugowOFGAuG7TxDRLod1Bd5JhpOikOiFqPmKjKEPtEA6mRCf7q3ulDyQ==",
2623 "license": "MIT",
2624 "dependencies": {
2625 - "@shikijs/types": "1.24.0",
2625 + "@shikijs/types": "1.24.2",
2626 "@shikijs/vscode-textmate": "^9.3.0"
2627 }
2628 },
2629 "node_modules/@shikijs/markdown-it": {
2630 - "version": "1.24.0",
2631 - "resolved": "https://registry.npmjs.org/@shikijs/markdown-it/-/markdown-it-1.24.0.tgz",
2632 - "integrity": "sha512-YjYg8jJoTO0cUXUNlFHTZWWFt4wSDOcRd2nM2aB1rnX5RqRlcqwfS2x1vQjlPqmUisv+/GHClvz7uKHeK7ZDBw==",
2630 + "version": "1.24.2",
2631 + "resolved": "https://registry.npmjs.org/@shikijs/markdown-it/-/markdown-it-1.24.2.tgz",
2632 + "integrity": "sha512-vLFRZYudSkrWWrtfBBZy7hM5mZjpC54zdxSNDn25nV6uVSilySmbdt70LyfiuTOtrKQ3p7fjuxojxqM/n6qVCg==",
2633 "license": "MIT",
2634 "dependencies": {
2635 "markdown-it": "^14.1.0",
2636 - "shiki": "1.24.0"
2636 + "shiki": "1.24.2"
2637 }
2638 },
2639 "node_modules/@shikijs/types": {
2640 - "version": "1.24.0",
2641 - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.24.0.tgz",
2642 - "integrity": "sha512-aptbEuq1Pk88DMlCe+FzXNnBZ17LCiLIGWAeCWhoFDzia5Q5Krx3DgnULLiouSdd6+LUM39XwXGppqYE0Ghtug==",
2640 + "version": "1.24.2",
2641 + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.24.2.tgz",
2642 + "integrity": "sha512-bdeWZiDtajGLG9BudI0AHet0b6e7FbR0EsE4jpGaI0YwHm/XJunI9+3uZnzFtX65gsyJ6ngCIWUfA4NWRPnBkQ==",
2643 "license": "MIT",
2644 "dependencies": {
2645 "@shikijs/vscode-textmate": "^9.3.0",
@@ -3117,14 +3117,14 @@
3117 }
3118 },
3119 "node_modules/@typescript-eslint/typescript-estree": {
3120 - "version": "8.17.0",
3121 - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.17.0.tgz",
3122 - "integrity": "sha512-JqkOopc1nRKZpX+opvKqnM3XUlM7LpFMD0lYxTqOTKQfCWAmxw45e3qlOCsEqEB2yuacujivudOFpCnqkBDNMw==",
3120 + "version": "8.18.0",
3121 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz",
3122 + "integrity": "sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==",
3123 "dev": true,
3124 - "license": "BSD-2-Clause",
3124 + "license": "MIT",
3125 "dependencies": {
3126 - "@typescript-eslint/types": "8.17.0",
3127 - "@typescript-eslint/visitor-keys": "8.17.0",
3126 + "@typescript-eslint/types": "8.18.0",
3127 + "@typescript-eslint/visitor-keys": "8.18.0",
3128 "debug": "^4.3.4",
3129 "fast-glob": "^3.3.2",
3130 "is-glob": "^4.0.3",
@@ -3139,16 +3139,14 @@
3139 "type": "opencollective",
3140 "url": "https://opencollective.com/typescript-eslint"
3141 },
3142 - "peerDependenciesMeta": {
3143 - "typescript": {
3144 - "optional": true
3145 - }
3142 + "peerDependencies": {
3143 + "typescript": ">=4.8.4 <5.8.0"
3144 }
3145 },
3146 "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/types": {
3149 - "version": "8.17.0",
3150 - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.17.0.tgz",
3151 - "integrity": "sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA==",
3147 + "version": "8.18.0",
3148 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.0.tgz",
3149 + "integrity": "sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==",
3150 "dev": true,
3151 "license": "MIT",
3152 "engines": {
@@ -3160,13 +3158,13 @@
3158 }
3159 },
3160 "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/visitor-keys": {
3163 - "version": "8.17.0",
3164 - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.17.0.tgz",
3165 - "integrity": "sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg==",
3161 + "version": "8.18.0",
3162 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz",
3163 + "integrity": "sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==",
3164 "dev": true,
3165 "license": "MIT",
3166 "dependencies": {
3169 - "@typescript-eslint/types": "8.17.0",
3167 + "@typescript-eslint/types": "8.18.0",
3168 "eslint-visitor-keys": "^4.2.0"
3169 },
3170 "engines": {
@@ -3224,9 +3222,9 @@
3222 }
3223 },
3224 "node_modules/@ungap/structured-clone": {
3227 - "version": "1.2.0",
3228 - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
3229 - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
3225 + "version": "1.2.1",
3226 + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz",
3227 + "integrity": "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==",
3228 "license": "ISC"
3229 },
3230 "node_modules/@vitejs/plugin-vue": {
@@ -12768,15 +12766,15 @@
12766 }
12767 },
12768 "node_modules/shiki": {
12771 - "version": "1.24.0",
12772 - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.24.0.tgz",
12773 - "integrity": "sha512-qIneep7QRwxRd5oiHb8jaRzH15V/S8F3saCXOdjwRLgozZJr5x2yeBhQtqkO3FSzQDwYEFAYuifg4oHjpDghrg==",
12769 + "version": "1.24.2",
12770 + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.24.2.tgz",
12771 + "integrity": "sha512-TR1fi6mkRrzW+SKT5G6uKuc32Dj2EEa7Kj0k8kGqiBINb+C1TiflVOiT9ta6GqOJtC4fraxO5SLUaKBcSY38Fg==",
12772 "license": "MIT",
12773 "dependencies": {
12776 - "@shikijs/core": "1.24.0",
12777 - "@shikijs/engine-javascript": "1.24.0",
12778 - "@shikijs/engine-oniguruma": "1.24.0",
12779 - "@shikijs/types": "1.24.0",
12774 + "@shikijs/core": "1.24.2",
12775 + "@shikijs/engine-javascript": "1.24.2",
12776 + "@shikijs/engine-oniguruma": "1.24.2",
12777 + "@shikijs/types": "1.24.2",
12778 "@shikijs/vscode-textmate": "^9.3.0",
12779 "@types/hast": "^3.0.4"
12780 }
frontend/package.json
+7 -7
@@ -41,7 +41,7 @@
41 "@fontsource/jetbrains-mono": "^5.1.1",
42 "@fontsource/lexend": "^5.1.1",
43 "@fontsource/public-sans": "^5.1.1",
44 - "@shikijs/markdown-it": "^1.24.0",
44 + "@shikijs/markdown-it": "^1.24.2",
45 "@tailwindcss/container-queries": "^0.1.1",
46 "@vueuse/core": "^12.0.0",
47 "axios": "^1.7.9",
@@ -62,7 +62,7 @@
62 "pinia": "^2.3.0",
63 "pinia-plugin-persistedstate": "^4.1.3",
64 "secure-ls": "^2.0.0",
65 - "shiki": "^1.24.0",
65 + "shiki": "^1.24.2",
66 "validator": "^13.12.0",
67 "vue": "^3.5.13",
68 "vue-advanced-cropper": "^2.8.9",
@@ -75,12 +75,12 @@
75 "vuedraggable": "^4.1.0"
76 },
77 "optionalDependencies": {
78 - "@rollup/rollup-linux-x64-gnu": "^4.28.0"
78 + "@rollup/rollup-linux-x64-gnu": "^4.28.1"
79 },
80 "devDependencies": {
81 "@antfu/eslint-config": "^3.11.2",
82 "@clack/prompts": "^0.8.2",
83 - "@iconify/vue": "^4.1.2",
83 + "@iconify/vue": "^4.2.0",
84 "@tsconfig/node20": "^20.1.4",
85 "@types/bytes": "^3.1.5",
86 "@types/file-saver": "^2.0.7",
@@ -121,9 +121,9 @@
121 },
122 "pnpm": {
123 "overrides": {
124 - "@typescript-eslint/eslint-plugin": "^8.17.0",
124 + "@typescript-eslint/eslint-plugin": "^8.18.0",
125 "@typescript-eslint/eslint-plugin>eslint": "$eslint",
126 - "@typescript-eslint/parser": "^8.17.0",
126 + "@typescript-eslint/parser": "^8.18.0",
127 "@typescript-eslint/parser>eslint": "$eslint",
128 "eslint": "$eslint"
129 }
@@ -135,6 +135,6 @@
135 "@typescript-eslint/parser": {
136 "eslint": "^9.16.0"
137 },
138 - "@typescript-eslint/typescript-estree": "^8.17.0"
138 + "@typescript-eslint/typescript-estree": "^8.18.0"
139 }
140 }
frontend/src/components/customers/CustomerCreationButton.vue
+64 -8
@@ -1,10 +1,31 @@
1 <template>
2 - <n-button size="small" type="primary" @click="showAddCustomer = true">
3 - <template #icon>
4 - <Icon :name="AddUserIcon" :size="14"></Icon>
5 - </template>
6 - Add Customer
7 - </n-button>
2 + <LicenseFeatureCheck
3 + :feature="licenseKey"
4 + :disabled="licenseDisabled"
5 + feedback="tooltip"
6 + @response="
7 + (() => {
8 + licenseChecked = true
9 + licenseResponse = $event
10 + })()
11 + "
12 + >
13 + <n-button
14 + :size="size || 'small'"
15 + type="primary"
16 + :loading="!licenseChecked && !licenseDisabled"
17 + :disabled="!licenseChecked || !licenseResponse || disabled"
18 + @click="showAddCustomer = true"
19 + >
20 + <template #icon>
21 + <Icon :name="AddUserIcon" :size="14"></Icon>
22 + </template>
23 + <div class="flex items-center gap-2">
24 + <span>Add Customer</span>
25 + <Icon v-if="!licenseResponse && licenseChecked" :name="LockIcon" :size="14" />
26 + </div>
27 + </n-button>
28 + </LicenseFeatureCheck>
29
30 <n-drawer
31 v-model:show="showAddCustomer"
@@ -20,22 +41,48 @@
41 </template>
42
43 <script setup lang="ts">
44 +import type { LicenseFeatures } from "@/types/license.d"
45 +import type { Size } from "naive-ui/es/button/src/interface"
46 import Icon from "@/components/common/Icon.vue"
47 +import LicenseFeatureCheck from "@/components/license/LicenseFeatureCheck.vue"
48 import { NButton, NDrawer, NDrawerContent } from "naive-ui"
25 -import { ref, watch } from "vue"
49 +import { computed, ref, watch } from "vue"
50 import CustomerForm from "./CustomerForm.vue"
51
52 +const { customersCount, disabled, size } = defineProps<{
53 + customersCount?: number
54 + disabled?: boolean
55 + size?: Size
56 +}>()
57 +
58 const emit = defineEmits<{
59 (e: "submitted"): void
60 }>()
61
62 const openForm = defineModel<boolean | undefined>("openForm", { default: false })
63
64 +const LockIcon = "carbon:locked"
65 const AddUserIcon = "carbon:user-follow"
35 -
66 const customerFormCTX = ref<{ reset: () => void } | null>(null)
67 const showAddCustomer = ref(false)
68
69 +const licenseChecked = ref(!customersCount)
70 +const licenseResponse = ref(!customersCount)
71 +const licenseDisabled = computed<boolean>(() => !customersCount)
72 +const licenseKey = computed<LicenseFeatures>(() => {
73 + let key: LicenseFeatures = "MSSP 5"
74 +
75 + if (customersCount && customersCount >= 5) {
76 + key = "MSSP 10"
77 + }
78 +
79 + if (customersCount && customersCount >= 10) {
80 + key = "MSSP Unlimited"
81 + }
82 +
83 + return key
84 +})
85 +
86 watch(showAddCustomer, val => {
87 if (!val) {
88 openForm.value = false
@@ -52,4 +99,13 @@ watch(
99 },
100 { immediate: true }
101 )
102 +
103 +watch(
104 + licenseKey,
105 + () => {
106 + licenseResponse.value = !customersCount
107 + licenseChecked.value = !customersCount
108 + },
109 + { immediate: true }
110 +)
111 </script>
frontend/src/components/customers/CustomersList.vue
+2 -2
@@ -39,7 +39,7 @@ import CustomerItem from "./CustomerItem.vue"
39
40 const props = defineProps<{ highlight: string | null | undefined; reload?: boolean }>()
41 const emit = defineEmits<{
42 - (e: "reloaded"): void
42 + (e: "loaded", value: number): void
43 }>()
44
45 const { highlight, reload } = toRefs(props)
@@ -60,6 +60,7 @@ function getCustomers() {
60 .then(res => {
61 if (res.data.success) {
62 customersList.value = res.data?.customers || []
63 + emit("loaded", customersList.value.length || 0)
64 } else {
65 message.warning(res.data?.message || "An error occurred. Please try again later.")
66 }
@@ -69,7 +70,6 @@ function getCustomers() {
70 })
71 .finally(() => {
72 loadingCustomers.value = false
72 - emit("reloaded")
73 })
74 }
75
frontend/src/components/incidentManagement/alerts/AlertAsset.vue
+20 -2
@@ -52,25 +52,39 @@
52 :title="assetNameTruncated"
53 segmented
54 >
55 - <div class="flex flex-wrap justify-end gap-3 p-6">
55 + <LicenseFeatureCheck
56 + feature="SOCFORTRESS AI"
57 + @response="
58 + (() => {
59 + licenseChecked = true
60 + licenseResponse = $event
61 + })()
62 + "
63 + />
64 + <n-spin :show="!licenseChecked" content-class="flex flex-wrap justify-end gap-3 p-6" :size="18">
65 <AIVelociraptorArtifactRecommendationButton
66 :index-id="asset.index_id"
67 :index-name="asset.index_name"
68 :agent-id="asset.agent_id"
69 :alert-id="asset.alert_linked"
70 + :force-license-response="licenseResponse"
71 />
72 <AIWazuhExclusionRuleButton
73 :index-id="asset.index_id"
74 :index-name="asset.index_name"
75 :alert-id="asset.alert_linked"
76 + :force-license-response="licenseResponse"
77 />
78 <AIAnalystButton
79 :index-id="asset.index_id"
80 :index-name="asset.index_name"
81 :alert-id="asset.alert_linked"
82 + :force-license-response="licenseResponse"
83 />
72 - </div>
84 + </n-spin>
85 +
86 <n-divider class="!my-0" />
87 +
88 <n-tabs type="line" animated :tabs-padding="24">
89 <n-tab-pane name="Info" tab="Info" display-directive="show">
90 <AlertAssetInfo :asset />
@@ -170,6 +184,7 @@ const ThreatIntelProcessEvaluationProvider = defineAsyncComponent(
184 )
185 const ArtifactsCollect = defineAsyncComponent(() => import("@/components/artifacts/ArtifactsCollect.vue"))
186 const CodeSource = defineAsyncComponent(() => import("@/components/common/CodeSource.vue"))
187 +const LicenseFeatureCheck = defineAsyncComponent(() => import("@/components/license/LicenseFeatureCheck.vue"))
188
189 const ViewIcon = "iconoir:eye-alt"
190 const LinkIcon = "carbon:launch"
@@ -182,6 +197,9 @@ const alertContext = ref<AlertContext | null>(null)
197 const processNameList = computed<string[]>(() => alertContext.value?.context?.process_name || [])
198 const isInvestigationAvailable = computed(() => processNameList.value.length)
199
200 +const licenseChecked = ref(false)
201 +const licenseResponse = ref(false)
202 +
203 watch(showDetails, val => {
204 if (val && !alertContext.value) {
205 getAlertContext(asset.alert_context_id)
frontend/src/components/license/LicenseFeatureCheck.vue
+21 -5
@@ -80,9 +80,14 @@ import Api from "@/api"
80 import Icon from "@/components/common/Icon.vue"
81 import { useGoto } from "@/composables/useGoto"
82 import { NButton, NCard, NModal, NTooltip } from "naive-ui"
83 -import { onBeforeMount, ref, watch } from "vue"
83 +import { ref, watch, watchEffect } from "vue"
84
85 -const { feature, feedback } = defineProps<{ feature: LicenseFeatures; feedback?: "overlay" | "alert" | "tooltip" }>()
85 +const { feature, feedback, disabled, forceShowFeedback } = defineProps<{
86 + feature: LicenseFeatures
87 + feedback?: "overlay" | "alert" | "tooltip"
88 + disabled?: boolean
89 + forceShowFeedback?: boolean
90 +}>()
91
92 const emit = defineEmits<{
93 (e: "response", value: boolean): void
@@ -95,11 +100,12 @@ const LicenseIcon = "carbon:license"
100 const AlertIcon = "mdi:alert-outline"
101 const loading = ref(false)
102 const { gotoLicense } = useGoto()
98 -const showFeedback = ref(false)
103 +const showFeedback = ref(forceShowFeedback ?? false)
104 const showModal = ref(false)
105
106 function checkFeature(feature: LicenseFeatures) {
107 loading.value = true
108 +
109 Api.license
110 .isFeatureEnabled(feature)
111 .then(res => {
@@ -133,7 +139,17 @@ watch(loading, val => {
139 }
140 })
141
136 -onBeforeMount(() => {
137 - checkFeature(feature)
142 +watch(
143 + [() => disabled, () => feature],
144 + values => {
145 + if (!values[0]) {
146 + checkFeature(values[1])
147 + }
148 + },
149 + { immediate: true }
150 +)
151 +
152 +watchEffect(() => {
153 + showFeedback.value = forceShowFeedback ?? false
154 })
155 </script>
frontend/src/components/license/deprecated/LicenseEditor.vue
+1
@@ -112,6 +112,7 @@
112 </template>
113
114 <script setup lang="ts">
115 +/** @deprecated */
116 import type { NewLicensePayload } from "@/api/endpoints/license"
117 import type { LicenseKey } from "@/types/license.d"
118 import Api from "@/api"
frontend/src/components/threatIntel/AIAnalystButton.vue
+20 -4
@@ -3,6 +3,8 @@
3 <LicenseFeatureCheck
4 feature="SOCFORTRESS AI"
5 feedback="tooltip"
6 + :disabled="disabledLicenseCheck"
7 + :force-show-feedback="disabledLicenseCheck && !licenseResponse"
8 @response="
9 (() => {
10 licenseChecked = true
@@ -102,12 +104,19 @@ import Api from "@/api"
104 import Icon from "@/components/common/Icon.vue"
105 import LicenseFeatureCheck from "@/components/license/LicenseFeatureCheck.vue"
106 import { NButton, NModal, NTabPane, NTabs, useMessage } from "naive-ui"
105 -import { defineAsyncComponent, ref } from "vue"
107 +import { defineAsyncComponent, ref, watchEffect } from "vue"
108
107 -const { indexName, indexId, alertId, size } = defineProps<{
109 +const {
110 + indexName,
111 + indexId,
112 + alertId,
113 + forceLicenseResponse = undefined,
114 + size
115 +} = defineProps<{
116 indexName: string
117 indexId: string
118 alertId: number
119 + forceLicenseResponse?: boolean
120 size?: Size
121 }>()
122
@@ -121,8 +130,9 @@ const loading = ref<boolean>(false)
130 const message = useMessage()
131 const analysisResponse = ref<AiAnalysisResponse | null>(null)
132 const licenseChecking = ref(false)
124 -const licenseChecked = ref(false)
125 -const licenseResponse = ref(false)
133 +const licenseChecked = ref(forceLicenseResponse !== undefined)
134 +const licenseResponse = ref(forceLicenseResponse ?? false)
135 +const disabledLicenseCheck = ref(forceLicenseResponse !== undefined)
136
137 function openResponse() {
138 showModal.value = true
@@ -148,4 +158,10 @@ function analysis() {
158 loading.value = false
159 })
160 }
161 +
162 +watchEffect(() => {
163 + licenseResponse.value = forceLicenseResponse ?? false
164 + licenseChecked.value = forceLicenseResponse !== undefined
165 + disabledLicenseCheck.value = forceLicenseResponse !== undefined
166 +})
167 </script>
frontend/src/components/threatIntel/AIVelociraptorArtifactRecommendationButton.vue
+21 -4
@@ -3,6 +3,8 @@
3 <LicenseFeatureCheck
4 feature="SOCFORTRESS AI"
5 feedback="tooltip"
6 + :disabled="disabledLicenseCheck"
7 + :force-show-feedback="disabledLicenseCheck && !licenseResponse"
8 @response="
9 (() => {
10 licenseChecked = true
@@ -72,13 +74,21 @@ import CardEntity from "@/components/common/cards/CardEntity.vue"
74 import Icon from "@/components/common/Icon.vue"
75 import LicenseFeatureCheck from "@/components/license/LicenseFeatureCheck.vue"
76 import { NButton, NEmpty, NModal, useMessage } from "naive-ui"
75 -import { ref } from "vue"
77 +import { ref, watchEffect } from "vue"
78
77 -const { indexName, indexId, agentId, alertId, size } = defineProps<{
79 +const {
80 + indexName,
81 + indexId,
82 + agentId,
83 + alertId,
84 + forceLicenseResponse = undefined,
85 + size
86 +} = defineProps<{
87 indexName: string
88 indexId: string
89 agentId: string
90 alertId: number
91 + forceLicenseResponse?: boolean
92 size?: Size
93 }>()
94
@@ -89,8 +99,9 @@ const loading = ref<boolean>(false)
99 const message = useMessage()
100 const analysisResponse = ref<AiVelociraptorArtifactRecommendationResponse | null>(null)
101 const licenseChecking = ref(false)
92 -const licenseChecked = ref(false)
93 -const licenseResponse = ref(false)
102 +const licenseChecked = ref(forceLicenseResponse !== undefined)
103 +const licenseResponse = ref(forceLicenseResponse ?? false)
104 +const disabledLicenseCheck = ref(forceLicenseResponse !== undefined)
105
106 function openResponse() {
107 showModal.value = true
@@ -117,4 +128,10 @@ function analysis() {
128 loading.value = false
129 })
130 }
131 +
132 +watchEffect(() => {
133 + licenseResponse.value = forceLicenseResponse ?? false
134 + licenseChecked.value = forceLicenseResponse !== undefined
135 + disabledLicenseCheck.value = forceLicenseResponse !== undefined
136 +})
137 </script>
frontend/src/components/threatIntel/AIWazuhExclusionRuleButton.vue
+20 -4
@@ -3,6 +3,8 @@
3 <LicenseFeatureCheck
4 feature="SOCFORTRESS AI"
5 feedback="tooltip"
6 + :disabled="disabledLicenseCheck"
7 + :force-show-feedback="disabledLicenseCheck && !licenseResponse"
8 @response="
9 (() => {
10 licenseChecked = true
@@ -64,12 +66,19 @@ import Api from "@/api"
66 import Icon from "@/components/common/Icon.vue"
67 import LicenseFeatureCheck from "@/components/license/LicenseFeatureCheck.vue"
68 import { NButton, NEmpty, NModal, useMessage } from "naive-ui"
67 -import { defineAsyncComponent, ref } from "vue"
69 +import { defineAsyncComponent, ref, watchEffect } from "vue"
70
69 -const { indexName, indexId, alertId, size } = defineProps<{
71 +const {
72 + indexName,
73 + indexId,
74 + alertId,
75 + forceLicenseResponse = undefined,
76 + size
77 +} = defineProps<{
78 indexName: string
79 indexId: string
80 alertId: number
81 + forceLicenseResponse?: boolean
82 size?: Size
83 }>()
84
@@ -83,8 +92,9 @@ const loading = ref<boolean>(false)
92 const message = useMessage()
93 const analysisResponse = ref<AiWazuhExclusionRuleResponse | null>(null)
94 const licenseChecking = ref(false)
86 -const licenseChecked = ref(false)
87 -const licenseResponse = ref(false)
95 +const licenseChecked = ref(forceLicenseResponse !== undefined)
96 +const licenseResponse = ref(forceLicenseResponse ?? false)
97 +const disabledLicenseCheck = ref(forceLicenseResponse !== undefined)
98
99 function openResponse() {
100 showModal.value = true
@@ -118,4 +128,10 @@ function analysis() {
128 loading.value = false
129 })
130 }
131 +
132 +watchEffect(() => {
133 + licenseResponse.value = forceLicenseResponse ?? false
134 + licenseChecked.value = forceLicenseResponse !== undefined
135 + disabledLicenseCheck.value = forceLicenseResponse !== undefined
136 +})
137 </script>
frontend/src/types/license.d.ts
+10 -1
@@ -41,7 +41,16 @@ export interface LicenseDataObject {
41 intValue: number
42 }
43
44 -export type LicenseFeatures = "REPORTING" | "THREAT INTEL" | "HUNTRESS" | "MIMECAST" | "CARBONBLACK" | "SOCFORTRESS AI"
44 +export type LicenseFeatures =
45 + | "REPORTING"
46 + | "THREAT INTEL"
47 + | "HUNTRESS"
48 + | "MIMECAST"
49 + | "CARBONBLACK"
50 + | "SOCFORTRESS AI"
51 + | "MSSP Unlimited"
52 + | "MSSP 10"
53 + | "MSSP 5"
54
55 export type LicenseKey = `${string}-${string}-${string}-${string}`
56
frontend/src/views/Customers.vue
+19 -2
@@ -1,8 +1,23 @@
1 <template>
2 <div class="page">
3 - <CustomersList :highlight="highlight" :reload="reload" @reloaded="reload = false">
3 + <CustomersList
4 + :highlight="highlight"
5 + :reload
6 + @loaded="
7 + (() => {
8 + customersCount = $event
9 + reload = false
10 + firstLoad = true
11 + })()
12 + "
13 + >
14 <CustomerDefaultSettingsButton />
5 - <CustomerCreationButton v-model:open-form="openForm" @submitted="reload = true" />
15 + <CustomerCreationButton
16 + v-model:open-form="openForm"
17 + :customers-count
18 + :disabled="!firstLoad"
19 + @submitted="reload = true"
20 + />
21 </CustomersList>
22 </div>
23 </template>
@@ -20,7 +35,9 @@ const router = useRouter()
35
36 const highlight = ref<string | undefined>(undefined)
37 const reload = ref(false)
38 +const firstLoad = ref(false)
39 const openForm = ref(false)
40 +const customersCount = ref<undefined | number>(undefined)
41
42 function setOpenForm() {
43 if (!openForm.value) {