Skip to content

Commit 9521c25

Browse files
authored
Merge pull request #94 from rapid7/ci
Plugin utilization validator
2 parents fb623c3 + 11de16e commit 9521c25

File tree

12 files changed

+1673
-4
lines changed

12 files changed

+1673
-4
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ to simulate the `--all` flag.
4949

5050
## Changelog
5151

52+
* 2.20.0 - Add plugin utilization workflow validator | Fix issue where numbers in screenshot titles would cause validation to fail
5253
* 2.19.0 - Add new `example` input to whitelist in SpecPropertiesValidator
5354
* 2.18.0 - Add .icon file validator
5455
* 2.17.2 - Fix to remove some common words from the profanity validator

icon_validator/rules/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from icon_validator.rules.workflow_validators.workflow_description_validator import *
5353
from icon_validator.rules.workflow_validators.workflow_name_validator import *
5454
from icon_validator.rules.workflow_validators.workflow_icon_validator import *
55+
from icon_validator.rules.workflow_validators.workflow_help_plugin_utilization_validator import *
5556

5657
# The order of this list is the execution order of the validators.
5758
VALIDATORS = [
@@ -106,5 +107,6 @@
106107
WorkflowDescriptionValidator(),
107108
WorkflowNameValidator(),
108109
WorkflowProfanityValidator(),
110+
WorkflowHelpPluginUtilizationValidator(),
109111
WorkflowICONFileValidator()
110112
]
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
from icon_validator.rules.validator import KomandPluginValidator
2+
from icon_validator.exceptions import ValidationException
3+
from icon_plugin_spec.plugin_spec import KomandPluginSpec
4+
5+
import os
6+
import json
7+
import re
8+
9+
10+
class _Plugin(object):
11+
12+
def __init__(self, name: str, version: str):
13+
self.plugin = name
14+
self.version = version
15+
16+
def __eq__(self, other):
17+
return (self.plugin == other.plugin) and (self.version == other.version)
18+
19+
def __hash__(self):
20+
return hash((self.plugin, self.version))
21+
22+
23+
class WorkflowHelpPluginUtilizationValidator(KomandPluginValidator):
24+
25+
@staticmethod
26+
def load_workflow_file(spec: KomandPluginSpec) -> dict:
27+
"""
28+
Load a workflow file as KomandPluginSpec into a dictionary
29+
:param spec: .icon workflow
30+
:return: Workflow spec as a dictionary
31+
"""
32+
workflow_directory = spec.directory
33+
34+
for file_name in os.listdir(workflow_directory):
35+
if not (file_name.endswith(".icon")):
36+
continue
37+
38+
with open(f"{workflow_directory}/{file_name}") as json_file:
39+
try:
40+
workflow_file = json.load(json_file)
41+
except json.JSONDecodeError:
42+
raise ValidationException(
43+
"The .icon file is not in JSON format. Try exporting the .icon file again")
44+
45+
return workflow_file
46+
47+
@staticmethod
48+
def extract_workflow(workflow_file: dict) -> dict:
49+
"""
50+
Returns a workflow with metadata and step information
51+
:param workflow_file: Dictionary containing workflow information
52+
:return: Workflow metadata and step information as a dict
53+
"""
54+
try:
55+
workflow = workflow_file["kom"]["workflowVersions"][0]
56+
except KeyError:
57+
raise ValidationException("The .icon file is not formatted correctly. Try exporting the .icon file again")
58+
59+
return workflow
60+
61+
@staticmethod
62+
def extract_plugins_used(workflow: dict) -> [dict]:
63+
64+
# Raw list of plugins
65+
plugin_list = list()
66+
try:
67+
for step_id in workflow["steps"]:
68+
step: dict = workflow["steps"][step_id]
69+
70+
# We only care about plugin steps, so continue if it is not
71+
if "plugin" not in step.keys():
72+
continue
73+
74+
plugin_name = step["plugin"]["name"]
75+
plugin_version = step["plugin"]["slugVersion"]
76+
77+
plugin = _Plugin(name=plugin_name, version=plugin_version)
78+
plugin_list.append(plugin)
79+
80+
# Once the loop is done, count unique items
81+
plugins_and_counts = []
82+
83+
for plugin in set(plugin_list):
84+
count = plugin_list.count(plugin)
85+
plugins_and_counts.append({**plugin.__dict__, "count": count})
86+
87+
return plugins_and_counts
88+
89+
except KeyError:
90+
raise ValidationException("The .icon file is not formatted correctly. Try exporting the .icon file again")
91+
92+
@staticmethod
93+
def extract_plugins_in_help(help_str: str) -> list:
94+
"""
95+
Takes the help.md file as a string and extracts plugin version and count as a list of dictionaries
96+
"""
97+
# regex to isolate the plugin utilization table
98+
regex = r"\|Plugin\|Version\|Count\|.*?#"
99+
100+
plugins_utilized = re.findall(regex, help_str, re.DOTALL)
101+
# Split each line into a sub string
102+
plugins_list = plugins_utilized[0].split("\n")
103+
# remove trailing and leading lines so that only plugin utilization data is left
104+
plugins_list = list(
105+
filter(lambda item: item.startswith("|") and not (item.startswith("|Plugin") or item.startswith("|-")),
106+
plugins_list))
107+
plugins_dict_list = list()
108+
# Build dictionary for each plugin e.g. {'Plugin': 'ExtractIt', 'Version': '1.1.6', 'Count': 1}
109+
# then append to a list
110+
for plugin in plugins_list:
111+
temp = plugin.split("|")
112+
plugins_dict_list.append({"plugin": temp[1], "version": temp[2], "count": int(temp[3])})
113+
return plugins_dict_list
114+
115+
def validate(self, spec):
116+
workflow_file = WorkflowHelpPluginUtilizationValidator.load_workflow_file(spec)
117+
workflow = WorkflowHelpPluginUtilizationValidator.extract_workflow(workflow_file)
118+
plugins_used = WorkflowHelpPluginUtilizationValidator.extract_plugins_used(workflow)
119+
plugin_in_help = WorkflowHelpPluginUtilizationValidator.extract_plugins_in_help(spec.raw_help())
120+
for plugin in plugins_used:
121+
if plugin not in plugin_in_help:
122+
raise ValidationException("The following plugin was found in the .icon file,"
123+
f" but not in the help file {plugin}")
124+
for plugin in plugin_in_help:
125+
if plugin not in plugins_used:
126+
raise ValidationException("The following plugin was found in the help file,"
127+
f" but not in the .icon file {plugin}")

icon_validator/rules/workflow_validators/workflow_screenshot_validator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def validate_title(title):
4646
# This is OK: Member Of
4747
# This is NOT OK: Type Of String
4848
raise ValidationException("Title contains a capitalized 'Of' when it should not.")
49-
elif not word[0].isupper() and not word.capitalize() in title_validation_list:
49+
elif not word[0].isupper() and not word[0].isnumeric() and not word.capitalize() in title_validation_list:
5050
if not word.lower() == "by" or word.lower() == "of":
5151
raise ValidationException(f"Title contains a lowercase '{word}' when it should not.")
5252

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
long_description = fh.read()
77

88
setup(name="insightconnect_integrations_validators",
9-
version="2.19.0",
9+
10+
version="2.20.0",
1011
description="Validator tooling for InsightConnect integrations",
1112
long_description=long_description,
1213
long_description_content_type="text/markdown",

unit_test/test_validate_workflow.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from icon_validator.rules.workflow_validators.workflow_description_validator import WorkflowDescriptionValidator
1818
from icon_validator.rules.workflow_validators.workflow_name_validator import WorkflowNameValidator
1919
from icon_validator.rules.workflow_validators.workflow_icon_validator import WorkflowICONFileValidator
20+
from icon_validator.rules.workflow_validators.workflow_help_plugin_utilization_validator import WorkflowHelpPluginUtilizationValidator
2021

2122

2223
class TestWorkflowValidate(unittest.TestCase):
@@ -158,3 +159,9 @@ def test_icon_validator(self):
158159
file_to_test = "workflow_bad_icon_file.spec.yaml"
159160
result = validate(directory_to_test, file_to_test, False, True, [WorkflowICONFileValidator()])
160161
self.assertTrue(result)
162+
163+
def test_workflow_plugin_utilization_validator(self):
164+
directory_to_test = "workflow_examples/plugin_utilization_tests"
165+
file_to_test = "workflow_bad_utilization.spec.yaml"
166+
result = validate(directory_to_test, file_to_test, False, True, [WorkflowHelpPluginUtilizationValidator()])
167+
self.assertTrue(result)
Loading
Loading

unit_test/workflow_examples/good_test/workflow.spec.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ resources:
1515
source_url: https://github.com/rapid7/insightconnect-workflows/tree/master/workflows/Automated_Indicator_Enrichment
1616
license_url: https://github.com/rapid7/insightconnect-workflows/blob/master/LICENSE
1717
screenshots:
18-
- name: Automated_Indicator_Enrichment_Job.png
19-
title: Example Job Output
18+
- name: artifact1.png
19+
title: Artifact Example
20+
- name: workflow1.png
21+
title: Workflow View 1
2022
- name: Automated_Indicator_Enrichment_Snapshot.png
2123
title: Workflow View
24+
- name: Automated_Indicator_Enrichment_Job.png
25+
title: Example Job Output

0 commit comments

Comments
 (0)