diff --git a/README.md b/README.md index 7665763..9ce8c57 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ be on your way to contributing! ## Changelog +* 2.47.34 - `EnableCacheValidator` - Require `enable_cache: true` for any plugin that defines triggers (orchestrator-only plugins included) so triggers do not silently fail on SDK 6.6.0+ | `CloudReadyValidator` - now only enforces `enable_cache: false` for Cloud Ready plugins without triggers * 2.47.33 - Updated dependencies to the latest versions * 2.47.32 - `CloudReadyValidator` - Allow `enable_cache: true` for Cloud Ready plugins that define triggers * 2.47.31 - `RuntimeValidator` - Detect caching via AST parsing to avoid false positives on the `cache` keyword | `DescriptionValidator` - Enforce 500 character description limit | `DockerValidator` - Skip build failures caused by dependency network issues diff --git a/icon_validator/__init__.py b/icon_validator/__init__.py index c407f21..a44adf6 100644 --- a/icon_validator/__init__.py +++ b/icon_validator/__init__.py @@ -1 +1 @@ -VERSION = "2.47.33" +VERSION = "2.47.34" diff --git a/icon_validator/rules/__init__.py b/icon_validator/rules/__init__.py index a3aed3b..d63fd08 100644 --- a/icon_validator/rules/__init__.py +++ b/icon_validator/rules/__init__.py @@ -41,6 +41,7 @@ from icon_validator.rules.plugin_validators.version_pin_validator import * from icon_validator.rules.plugin_validators.example_input_validator import * from icon_validator.rules.plugin_validators.cloud_ready_validator import * +from icon_validator.rules.plugin_validators.enable_cache_validator import * from icon_validator.rules.plugin_validators.supported_version_validator import * from icon_validator.rules.plugin_validators.unapproved_keywords_validator import * from icon_validator.rules.plugin_validators.help_example_validator import * @@ -104,6 +105,7 @@ EncodingValidator(), ExampleInputValidator(), CloudReadyValidator(), + EnableCacheValidator(), SupportedVersionValidator(), UnapprovedKeywordsValidator(), HelpExampleValidator(), @@ -144,6 +146,7 @@ EncodingValidator(), ExampleInputValidator(), CloudReadyValidator(), + EnableCacheValidator(), SupportedVersionValidator(), UnapprovedKeywordsValidator(), HelpExampleValidator(), diff --git a/icon_validator/rules/plugin_validators/cloud_ready_validator.py b/icon_validator/rules/plugin_validators/cloud_ready_validator.py index b55ca83..6234244 100755 --- a/icon_validator/rules/plugin_validators/cloud_ready_validator.py +++ b/icon_validator/rules/plugin_validators/cloud_ready_validator.py @@ -9,16 +9,11 @@ class CloudReadyValidator(KomandPluginValidator): @staticmethod def validate_enable_cache_in_plugin_spec(plugin_spec: dict): - # Plugin with triggers must have caching enabled and vice versa + # A Cloud Ready plugin without triggers must not enable caching. + # (The with-triggers case is enforced for all plugins by EnableCacheValidator.) has_triggers = bool(plugin_spec.get("triggers")) enable_cache = plugin_spec.get("enable_cache", False) - if has_triggers and not enable_cache: - raise ValidationException( - "'enable_cache' must be set to 'true' for a Cloud Ready plugin with triggers. " - "Please check this field in plugin.spec and try again." - ) - if not has_triggers and enable_cache: raise ValidationException( "'enable_cache' must be set to 'false' for a Cloud Ready plugin without triggers. " diff --git a/icon_validator/rules/plugin_validators/enable_cache_validator.py b/icon_validator/rules/plugin_validators/enable_cache_validator.py new file mode 100644 index 0000000..629b0f0 --- /dev/null +++ b/icon_validator/rules/plugin_validators/enable_cache_validator.py @@ -0,0 +1,23 @@ +from icon_validator.exceptions import ValidationException +from icon_validator.rules.validator import KomandPluginValidator + + +class EnableCacheValidator(KomandPluginValidator): + # Triggers rely on caching on SDK 6.6.0+, so any plugin defining triggers must set + # enable_cache: true. This applies to all plugins, hence it is separate from the + # Cloud Ready, Dockerfile-dependent checks in CloudReadyValidator. + + @staticmethod + def validate_enable_cache_with_triggers(plugin_spec: dict): + has_triggers = bool(plugin_spec.get("triggers")) + # strict: reject truthy strings like "false" + enable_cache = plugin_spec.get("enable_cache") is True + + if has_triggers and not enable_cache: + raise ValidationException( + "'enable_cache' must be set to 'true' for a plugin with triggers. " + "Please check this field in plugin.spec and try again." + ) + + def validate(self, spec): + EnableCacheValidator.validate_enable_cache_with_triggers(spec.spec_dictionary()) diff --git a/unit_test/plugin_examples/bad_plugin_triggers_no_enable_cache/plugin.spec.yaml b/unit_test/plugin_examples/bad_plugin_triggers_no_enable_cache/plugin.spec.yaml new file mode 100644 index 0000000..8a2408b --- /dev/null +++ b/unit_test/plugin_examples/bad_plugin_triggers_no_enable_cache/plugin.spec.yaml @@ -0,0 +1,218 @@ +plugin_spec_version: v2 +extension: plugin +products: [insightconnect] +name: abuseipdb +title: AbuseIPDB +description: Enables the look up of IP reports, provides list and details of blacklisted IPs, and submissions of abusive IPs +version: 5.0.6 +vendor: rapid7 +support: community +status: [] +resources: + source_url: https://github.com/rapid7/insightconnect-plugins/tree/master/abuseipdb + license_url: https://github.com/rapid7/insightconnect-plugins/blob/master/LICENSE + vendor_url: https://www.abuseipdb.com +tags: +- ip +- intelligence +- abuse +hub_tags: + use_cases: [data_enrichment, threat_detection_and_response] + keywords: [ip, intelligence, abuse] + features: [] +types: + + blacklisted: + ipAddress: + title: IP Address + description: IP Address of abusive IP + type: string + required: true + abuseConfidenceScore: + title: Abuse Confidence Score + description: Confidence that IP is abusive + type: string + required: true + + report: + reportedAt: + title: Reported At + description: Date and time of report + type: string + required: false + comment: + title: Comment + description: Comment by reporter + type: string + required: false + categories: + title: Categories + description: List of categories + type: '[]integer' + required: false + reporterId: + title: Reporter ID + description: ID number of reporter + type: integer + required: false + reporterCountryCode: + title: Reporter Country Code + description: Country code of the reporter + type: string + required: false + reporterCountryName: + title: Reporter Country Name + description: Name of country reporter is from + type: string + required: false + + reportedIPs: + ipAddress: + title: IP + type: string + description: IP Address of reported resource + numReports: + title: Number of Reports + type: integer + description: Number of reports of this IP + mostRecentReport: + title: Most Recent Report + type: string + description: Most recent report for this IP + abuseConfidenceScore: + title: Abuse Confidence Score + type: integer + description: Confidence that this IP is abusive + countryCode: + title: Country Code + type: string + description: Country code of IP + +connection: + credentials: + title: API Key + description: API key from account + type: credential_secret_key + required: true + example: e73h82c63847f3ff1h5216b556edh153h30430d73bchhe680f70h1d8885fb8bb130b46c7767d6886 + +actions: + check_ip: + title: Check IP + description: Look up an IP address in the database + input: + address: + title: IP Address + description: IPv4 or IPv6 address e.g. 198.51.100.100, ::1, must be subscribed to accept bitmask wider than 255.255.255.0 (/24) + type: string + required: true + example: 198.51.100.100 + days: + title: Days + description: Check for IP reports in the last x days + type: string + default: '30' + required: true + example: 30 + verbose: + title: Verbose + description: When set, reports will include the comment (if any) and the reporter's + user ID number (0 if reported anonymously) + type: boolean + default: true + required: true + example: true + output: + ipAddress: + title: IP Address + description: Queried IP Address + type: string + required: false + isPublic: + title: Is Public + description: Whether or not the IP Address is public + type: boolean + required: false + ipVersion: + title: IP Version + description: Version of IP Address + type: integer + required: false + isWhitelisted: + title: Is Whitelisted + description: Whether or not IP Address is whitelisted + type: boolean + required: false + abuseConfidenceScore: + title: Abuse Confidence Score + description: Confidence of Abuse + type: integer + required: false + countryCode: + title: Country Code + description: Code of country IP is registered in + type: string + required: false + usageType: + title: Usage Type + description: How IP is used + type: string + required: false + isp: + title: ISP + description: Internet Service Provider for IP + type: string + required: false + domain: + title: Domain + description: Domain Name of IP + type: string + required: false + countryName: + title: Country Name + description: Name of Country IP is registered in + type: string + required: false + totalReports: + title: Total Reports + description: Total number of reports of abuse + type: integer + required: false + numDistinctUsers: + title: Number of Distinct Users + description: Number of distinct users who reported IP + type: integer + required: false + lastReportedAt: + title: Last Reported At + description: Date of last report + type: string + required: false + reports: + title: Reports + description: List of reports + type: '[]report' + required: false + found: + title: Found + description: Whether an IP address was found in the database + type: boolean + required: false + +triggers: + new_message_received: + title: New Message Received + description: Trigger when a new message is received + input: + address: + title: IP Address + description: IPv4 or IPv6 address + type: string + required: true + example: 198.51.100.100 + output: + ipAddress: + title: IP Address + description: The IP address + type: string + required: false diff --git a/unit_test/test_validate_plugin/test_validate_plugin.py b/unit_test/test_validate_plugin/test_validate_plugin.py index d363658..54369a7 100644 --- a/unit_test/test_validate_plugin/test_validate_plugin.py +++ b/unit_test/test_validate_plugin/test_validate_plugin.py @@ -20,6 +20,7 @@ ) from icon_validator.rules.plugin_validators.cloud_ready_validator import CloudReadyValidator from icon_validator.rules.plugin_validators.confidential_validator import ConfidentialValidator +from icon_validator.rules.plugin_validators.enable_cache_validator import EnableCacheValidator from icon_validator.rules.plugin_validators.description_validator import DescriptionValidator from icon_validator.rules.plugin_validators.encoding_validator import EncodingValidator from icon_validator.rules.plugin_validators.example_input_validator import ExampleInputValidator @@ -475,20 +476,58 @@ def test_cloud_ready_validator_should_success_latest_version_string(self): except ValidationException: raise Exception("We do not expect the supplied docker string to fail. We should support ':latest'") - def test_cloud_ready_validator_triggers_enable_cache_false_should_fail(self): + def test_cloud_ready_validator_with_triggers_should_succeed(self): + # Cloud ready plugin with triggers and enable_cache: true should pass + directory_to_test = "plugin_examples/good_plugin_cloud_ready_with_triggers" + file_to_test = "plugin.spec.yaml" + result = validate(directory_to_test, file_to_test, False, True, [CloudReadyValidator()]) + self.assertEqual(result, 0) + + def test_enable_cache_validator_cloud_triggers_no_cache_should_fail(self): # Cloud ready plugin with triggers must have enable_cache: true directory_to_test = "plugin_examples/bad_plugin_cloud_ready_triggers_enable_cache_false" file_to_test = "plugin.spec.yaml" - result = validate(directory_to_test, file_to_test, False, True, [CloudReadyValidator()]) + result = validate(directory_to_test, file_to_test, False, True, [EnableCacheValidator()]) self.assertEqual(result, 1) - def test_cloud_ready_validator_with_triggers_should_succeed(self): - # Cloud ready plugin with triggers and enable_cache: true should pass + def test_enable_cache_validator_orchestrator_triggers_no_cache_should_fail(self): + # Orchestrator-only plugin (not cloud ready) with triggers must also have enable_cache: true + directory_to_test = "plugin_examples/bad_plugin_triggers_no_enable_cache" + file_to_test = "plugin.spec.yaml" + result = validate(directory_to_test, file_to_test, False, True, [EnableCacheValidator()]) + self.assertEqual(result, 1) + + def test_enable_cache_validator_triggers_with_cache_should_succeed(self): + # A plugin with triggers and enable_cache: true should pass (cloud_ready is ignored here) directory_to_test = "plugin_examples/good_plugin_cloud_ready_with_triggers" file_to_test = "plugin.spec.yaml" - result = validate(directory_to_test, file_to_test, False, True, [CloudReadyValidator()]) + result = validate(directory_to_test, file_to_test, False, True, [EnableCacheValidator()]) self.assertEqual(result, 0) + def test_enable_cache_validator_no_triggers_should_succeed(self): + # A plugin without triggers is not required to enable caching + directory_to_test = "plugin_examples/good_plugin" + file_to_test = "plugin.spec.yaml" + result = validate(directory_to_test, file_to_test, False, True, [EnableCacheValidator()]) + self.assertEqual(result, 0) + + @parameterized.expand([ + ("triggers_and_cache_true", {"triggers": {"a_trigger": {}}, "enable_cache": True}, False), + ("triggers_and_cache_absent", {"triggers": {"a_trigger": {}}}, True), + ("triggers_and_cache_false", {"triggers": {"a_trigger": {}}, "enable_cache": False}, True), + ("triggers_and_cache_string_false", {"triggers": {"a_trigger": {}}, "enable_cache": "false"}, True), + ("triggers_and_cache_string_true", {"triggers": {"a_trigger": {}}, "enable_cache": "true"}, True), + ("empty_triggers", {"triggers": {}, "enable_cache": False}, False), + ("no_triggers_no_cache", {"enable_cache": False}, False), + ("no_triggers_with_cache", {"enable_cache": True}, False), + ]) + def test_enable_cache_validator_logic(self, _name: str, plugin_spec: dict, should_raise: bool): + if should_raise: + with self.assertRaises(ValidationException): + EnableCacheValidator.validate_enable_cache_with_triggers(plugin_spec) + else: + EnableCacheValidator.validate_enable_cache_with_triggers(plugin_spec) + def test_acronym_validator_should_success(self): # example workflow in plugin_examples directory. Run tests with these files directory_to_test = "plugin_examples/good_plugin"