Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Regorus policy engine framework #3187

Merged
merged 31 commits into from
Aug 28, 2024

Conversation

mgunnala
Copy link

@mgunnala mgunnala commented Aug 14, 2024

Description

This is PR 1 for policy enforcement within the guest agent. This PR adds the test Regorus executable and initial policy engine framework to call the executable. Guest agent functionality is not yet affected.

  • Regorus is utilized as the policy engine. Regorus is maintained as a separate open-source repository. Currently, static Rust MUSL-based executable is built separately and copied into the tests/data folder. The binary will eventually be added to the package via official build pipeline but for now, is kept in the test directory.
  • regorus.py implements add_policy, add_data, set_input, and eval_query functions by calling the Regorus executable via subprocess.
  • policy_engine.py implements a PolicyEngineConfigurator (responsible for conf checks and import) and PolicyEngine (responsible for higher level policy functions).
  • new "Debug.EnableExtensionPolicy" conf flag added to enable policy enforcement. Regorus is only imported if policy is enabled via conf and the platform is supported.
  • In this PR, extension installation is not affected (no policy enforcement code added to exthandlers.py). This functionality will be added in the next PR. Once functionality is complete, any errors thrown in policy enforcement code will block extension installation.
  • Unit tests were added for policy_engine.py and regorus.py. There is no change to agent functionality (PolicyEngine is not instantiated anywhere) so no end-to-end tests have been added yet.

PR information

  • The title of the PR is clear and informative.
  • There are a small number of commits, each of which has an informative message. This means that previously merged commits do not appear in the history of the PR. For information on cleaning up the commits in your pull request, see this page.
  • If applicable, the PR references the bug/issue that it fixes in the description.
  • New Unit tests were added for the changes made

Quality of Code and Contribution Guidelines

@mgunnala mgunnala marked this pull request as ready for review August 16, 2024 15:30
log_policy("Policy enforcement is disabled via configuration file.")
return

if not self._is_policy_supported():
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, we want to allow all extensions to be installed, but log that customer is trying to enable policy on unsupported distro.

Copy link
Author

@mgunnala mgunnala Aug 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline: all extensions should be blocked. I've updated the code to return an empty query result in this case. The next PR will use the empty query result to return an empty allow list, so that no extensions will be installed (ADO task 29144116, added TODO to code).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think empty list is not the right way to represent those errors. Both of them block extension execution, but empty list should just communicate the user that no extensions are allowed to execute, while errors should be very explicit about what is happening (try to enable on unsupported distro, invalid policy file, etc) so that the user can take the appropriate action

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, the extension status message should be clear that extension processing is blocked because this is an unsupported distro (or whatever the reason is)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated error handling to raise exceptions

Copy link
Member

@narrieta narrieta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments on the Agent code. I did not review the tests, since I think addressing some of my comment will require changes in the implementation/behavior

azurelinuxagent/ga/policy/policy_engine.py Show resolved Hide resolved

# Define support matrix for Regorus and policy engine feature.
# Dict in the format: { distro:min_supported_version }
POLICY_SUPPORTED_DISTROS = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: suggest POLICY_SUPPORTED_DISTROS_MIN_VERSIONS

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

# Dict in the format: { distro:min_supported_version }
POLICY_SUPPORTED_DISTROS = {
'ubuntu': FlexibleVersion('16.04'),
'mariner': FlexibleVersion('2')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's add 'azurelinux' '3'

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

'ubuntu': FlexibleVersion('16.04'),
'mariner': FlexibleVersion('2')
}
POLICY_SUPPORTED_ARCHITECTURE = ['x86_64', 'amd64']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double-check this, specially for arm64. it can also be aarch64

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have more telemetry on this now. I'll share with Manu

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not supporting arm64 until public preview. Added a TODO to update this list once we enable support.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should remove 'amd64' from this list. We don't have telemetry for this arch type:

cluster("azcore.centralus.kusto.windows.net").database("Fa").GuestAgentExtensionEvents
| where PreciseTimeStamp >= ago(30d)
| where OSVersion startswith "linux"
| summarize dcount(ContainerId) by KeywordName

KeywordName dcount_ContainerId
{"CpuArchitecture": "x86_64"} 739,107,991
  639,940,276
{"CpuArchitecture": "aarch64"} 3,092,416
{"CpuArchitecture": "i686"} 280

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed amd64

Log information to console and telemetry.
"""
if is_success:
logger.info("[Policy] " + formatted_string)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: not sure we need the "[Policy]" prefix

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it made the log file a little easier to read/parse, can remove if you think it adds too much clutter.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

PolicyEngineConfigurator._initialized = True

@staticmethod
def is_policy_configured():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename to is_policy_enabled

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed to is_policy_enforcement_enabled( ), to avoid confusion with the policy_engine_enabled property.

log_policy("Policy enforcement is disabled via configuration file.")
return

if not self._is_policy_supported():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think empty list is not the right way to represent those errors. Both of them block extension execution, but empty list should just communicate the user that no extensions are allowed to execute, while errors should be very explicit about what is happening (try to enable on unsupported distro, invalid policy file, etc) so that the user can take the appropriate action

return

# Regorus import should only be attempted after completing the above checks within
# the configurator, but the module itself needs to be accessible outside this class.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by PolicyEngine? shouldn't we move the initialization there then?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think importing here, only after checking that the feature is enabled, etc, used to make sense when the interface with regorus was going to be a set of Python bindings that may crash on some distros. Not sure what the motivation is with the current implementation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense. I've moved the initialization to policy engine.

Use: PolicyEngineConfigurator.get_instance()
"""
_instance = None # configurator is implemented as a singleton
_initialized = False # set to true on first init, even if Regorus import fails, we don't want to retry.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this state should be static, since we are trying to initialize only once per waagent service start.

Under some scenarios, for example the user tries to use the feature on an unsupported distro, the user should not need to restart the agent to fix the issue.

This initialization should probably be done on a per-goal state basis, unless it is very expensive.

Alternatively, some initialization could be done on a per-service-start basis, and some on a per-goal-state basis, but those should be clearly defined.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved initialization to PolicyEngine. Exthandlers.py will initialize the PolicyEngine on a per-goal state basis.

if arch not in POLICY_SUPPORTED_ARCHITECTURE:
return False
distro_info = get_distro()
distro_name = distro_info[0]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer DISTRO_NAME and DISTRO_VERSION over distro_info[0], [1]. Makes the code easier to read.

they are initialized like this

__distro__ = get_distro()
DISTRO_NAME = __distro__[0]
DISTRO_VERSION = __distro__[1]

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

Copy link
Member

@narrieta narrieta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed one file in my initial review. Additional comments.

import azurelinuxagent.ga.policy.regorus as regorus
PolicyEngineConfigurator._policy_enabled = True

except (ImportError, NameError) as ex:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How could ImportError and NameError happen?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed these errors, not relevant since we're no longer using a python wrapper

return

# Regorus import should only be attempted after completing the above checks within
# the configurator, but the module itself needs to be accessible outside this class.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think importing here, only after checking that the feature is enabled, etc, used to make sense when the interface with regorus was going to be a set of Python bindings that may crash on some distros. Not sure what the motivation is with the current implementation.

self._policy_engine_enabled = False
self._engine = None
try:
if PolicyEngineConfigurator.get_instance().get_policy_enabled():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This usage is weird, because get_policy_enabled() is an static method, so one does not need an instance to invoke it...

Why the call to get_instance()?

This is related to one of my comments above about all methods/properties of PolicyEngineConfigurator being static, so there is no need for a singleton instance

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed PolicyEngineConfigurator

def add_policy(self, policy_path):
"""Policy_path is expected to point to a valid Regorus file."""
if not os.path.exists(policy_path) or not policy_path.endswith('.rego'):
raise Exception("Policy path {0} is not a valid .rego file.".format(policy_path))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this check seems extremely weak... seems like we accept as valid any file, no matter its contents, as long as its name ends with ".rego"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see, the actual validation would be done by regorus and then we would need to handle the error reporting here.

a TODO comment would clarify thin

azurelinuxagent/ga/policy/regorus.py Outdated Show resolved Hide resolved
log_policy("Error: Failed to add data to Regorus policy engine. '{0}'".format(ex), is_success=False)

def set_input(self, input_json):
"""Input_json should be a JSON object."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you describe what is in that JSON object?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added


def eval_query(self, query):
missing_files = []
if self._policy_file is None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is weird. callers to eval_query() need to know that they must call add_policy(), set_input() and add_data() f first. How do they know that? why if the caller invokes those 3 methods to evaluate a query and then forgets to call one of them when trying to evaluate another query?

why not

def eval_query(self, policy_path, input_json, data, query):

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline - this is how the regorus library implements it, we're keeping the functions the same in case we want to change the underlying implementation in the future (ex switch to a python library). PolicyEngine init function calls add_policy() and add_data(), PolicyEngine.evaluate_query() takes input as a parameter and calls set_input().

self._policy_file = policy_path

def set_input(self, input_json):
"""Input_path is expected to point to a valid JSON object."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you describe what is in that JSON object?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

raise Exception("Input to policy engine is not a valid JSON object.")

def add_data(self, data):
"""Data parameter is expected to point to a valid json file."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you describe what is in that JSON file?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added, though I haven't included full details here, more explanation in the spec doc. Let me know if more detail is needed here specifically.

"-i", input_file.name, query]

# use subprocess.Popen instead of subprocess.run for Python 2 compatibility
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use shellutil.run_command() instead of Popen()

@@ -0,0 +1,125 @@
# Copyright (c) Microsoft Corporation.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be under the normal Agent license instead?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated to normal agent license since this will be packaged with agent

@staticmethod
def is_policy_enforcement_supported():
"""Check that both platform architecture and distro/version are supported."""
arch = platform.machine().lower()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have an osutil function for this: osutil.get_vm_arch()

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated to use osutil

if arch not in POLICY_SUPPORTED_ARCHITECTURE:
return False
__distro__ = get_distro()
DISTRO_NAME = __distro__[0]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, what I meant was to use the distro name and version already defined in azurelinuxagent.common.version

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that makes sense. Updated.

DISTRO_VERSION = __distro__[1]
try:
distro_version = DistroVersion(DISTRO_VERSION)
except ValueError:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this left-over code?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this function implementation based on the other comments, removed this block too.

"""This property tracks whether the feature is enabled and Regorus engine has been successfully initialized"""
return self._policy_engine_enabled

def evaluate_query(self, input_dict, query):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use a better name than "input_dict"?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"input_to_query" or "input_to_check" maybe?

}, ...
}

Expected format for query: "data.agent_extension_policy.extensions_to_download"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does "data.agent_extension_policy.extensions_to_download"? an extension name?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the value we're querying for in the regorus engine. We can query for "data", "data.agent_extension_policy", "data.agent_extension_policy.extensions_to_download", etc.

"data.agent_extension_policy.extensions_to_download" will return output in the format:

"extensions_to_download": { 
        "Microsoft.Azure.ActiveDirectory.AADSSHLoginForLinux": { 
            "downloadAllowed": true 
        }, 
        "Microsoft.Azure.Extensions.CustomScript": { 
            "downloadAllowed": false 
        } 
    }

...
I've put a full example in the spec.

try:
full_result = self._engine.eval_query(input_dict, query)
if not full_result.get('result') or not isinstance(full_result['result'], list):
raise PolicyError("query returned unexpected output with no 'result' list. Please validate rule file.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add the full_result to the message? it would be needed for debugging.

while doing that, adding the path to the rule file would also be helpful (it was mentioned before that there may be multiple fules files)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar comment for other messages

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added!

with patch('azurelinuxagent.ga.policy.policy_engine.get_osutil') as mock_get_osutil:
with patch('azurelinuxagent.ga.policy.policy_engine.conf.get_extension_policy_enabled', return_value=True):
with self.assertRaises(PolicyError, msg="Policy should not be enabled on unsupported architecture ARM64, should have raised exception."):
mock_osutil = MagicMock()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why mocking all of osutil instead of just mocking get_vm_arch? (if the code ends up doing other calls to utilities in osutil, the test may break)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks. I've fixed it.

all methods will be no-ops.

If any errors are thrown in regorus.py, they will be caught and handled here. add_policy, add_data,
and set_input will be no-ops, eval_query will return an empty dict
Copy link
Contributor

@maddieford maddieford Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment still true?

add_policy, add_data, and set_input will be no-ops, eval_query will return an empty dict

Seems like we updated to raise exc instead of no-op

Example format for query: "data.agent_extension_policy.extensions_to_download"
"""
# This method should never be called if policy is not enabled, this would be a developer error.
if not self.policy_engine_enabled:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if not self.policy_engine_enabled:
if not self._policy_engine_enabled:

or

Suggested change
if not self.policy_engine_enabled:
if not self.policy_engine_enabled():

Copy link
Member

@narrieta narrieta Aug 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoa! great catch @maddieford!

pylint disappoints me more often than I'd like. But if the linter fails, the next step would be unit tests, I guess

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maddieford not sure I'm understanding the issue - this came from a previous comment to make policy_engine_enabled( ) into a @Property method. My understanding is we can just access "self.policy_engine_enabled" since it's acting as a property. Is this not the case?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mgunnala - the parenthesis are missing from the function call. As written currently, that condition is always True

maddieford points out that you should add the parenthesis or use the private property

Copy link
Member

@narrieta narrieta Aug 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bad bug, easy to miss. The linter (or the unit tests) didn't point it out.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, it's a property!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought @Property methods in python didn't require the parentheses.

I have a unit test looking for exactly this scenario - "test_eval_query_should_throw_error_when_disabled()". It passes, and when I step through it, the function is being called as expected.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, i think we misread the code

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but, related to this property.. is this needed at all? see my other comment. looks like a left-over

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replied to your other comment

try:
full_result = self._engine.eval_query(input_to_check, query)
debug_info = "Rule file is located at '{0}'. \nFull query output: {1}".format(self._engine.rule_file, full_result)
if not full_result.get('result') or not isinstance(full_result['result'], list):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not full_result.get('result')
Are we checking for None here? Looks like 'result' value type is list so we should probably do explicit check for None here instead of 'not full_result...'

Suggested change
if not full_result.get('result') or not isinstance(full_result['result'], list):
if full_result.get('result') is None or not isinstance(full_result['result'], list):

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@mgunnala We avoid those implicit comparisons. We check "if foo/if not foo" only for actual bools. Comparisons against None, empty list, empty string, etc, should be explicit

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added explicit None checks for full_result, result, expression and value

if not full_result.get('result') or not isinstance(full_result['result'], list):
raise PolicyError("query returned unexpected output with no 'result' list. Please validate rule file. {0}".format(debug_info))
expressions = full_result['result'][0].get('expressions')
if not expressions or not isinstance(expressions, list) or len(expressions) == 0:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment here, check if expressions is None explicitly

if not expressions or not isinstance(expressions, list) or len(expressions) == 0:
raise PolicyError("query returned unexpected output with no 'expressions' list. {0}".format(debug_info))
value = expressions[0].get('value')
if not value:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


def get_extension_policy_enabled(conf=__conf__):
"""
Determine whether extension policy is enabled. If true, Regorus will be installed on the VM and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: missed this one before: "If true, Regorus will be installed on the VM". Regorus is always installed on the VM, no matter the value of the flag


class PolicyEngine(object):
"""
Implements policy engine API. Class will always be initialized, but if the Regorus import fails,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left-over comment; we removed the try/except of ImportError in a previous iteration

Implements policy engine API. Class will always be initialized, but if the Regorus import fails,
all methods will be no-ops.

If any errors are thrown in regorus.py, they will be caught and handled here. add_policy, add_data,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left-over comment -- i think those methods are now gone

Implements policy engine API. Class will always be initialized, but if the Regorus import fails,
all methods will be no-ops.

If any errors are thrown in regorus.py, they will be caught and handled here. add_policy, add_data,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are some related left-over comments in the unit tests as well

try:
self._engine = regorus.Engine(policy_file=policy_file, rule_file=rule_file)
self._policy_engine_enabled = True
except Exception as ex:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this try/catch a leftover of a previous iteration? at this point the initialization is just

        self._engine = None
        self._rule_file = rule_file
        self._policy_file = policy_file

Example format for query: "data.agent_extension_policy.extensions_to_download"
"""
# This method should never be called if policy is not enabled, this would be a developer error.
if not self.policy_engine_enabled:
Copy link
Member

@narrieta narrieta Aug 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoa! great catch @maddieford!

pylint disappoints me more often than I'd like. But if the linter fails, the next step would be unit tests, I guess

try:
full_result = self._engine.eval_query(input_to_check, query)
debug_info = "Rule file is located at '{0}'. \nFull query output: {1}".format(self._engine.rule_file, full_result)
if not full_result.get('result') or not isinstance(full_result['result'], list):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@mgunnala We avoid those implicit comparisons. We check "if foo/if not foo" only for actual bools. Comparisons against None, empty list, empty string, etc, should be explicit


@property
def policy_engine_enabled(self):
"""This property tracks whether the feature is enabled and Regorus engine has been successfully initialized"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left-over comment: "and Regorus engine has been successfully initialized""

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed this property

raise PolicyError(msg)

@classmethod
def log_policy(cls, msg, is_success=True, op=WALAEventOperation.Policy, send_event=True):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be private

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made private

@maddieford maddieford merged commit 321af32 into Azure:develop Aug 28, 2024
11 checks passed
@mgunnala mgunnala deleted the regorus_policy_engine branch September 3, 2024 01:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants