PP Core Concepts

Introduction

The Xylok Security Suite allows additional processing to be done on the returned command outputs. Post-processing (PP) can perform several functions:

  • Clean up output, simpilifying and focusing the original raw output into a more human-readable format

  • Make a finding status recommendation (compliant, non-compliant, not applicable, etc)

  • Make a finding comment recommendation (“This value is X but should be Y”)

  • Set metadata for the device it was run on, including software lists, host name, IPs, etc.

Post-processing scripts are written in Python. Each script is handed a context object which contains all of the information available and the scripts can then output their results using standard Python print() and functions on the context.Context object.

PP scripts run at both the individual command level and at the check level. Command-level scripts can make recommendations for the check as a whole or simply clean up data for the check-level script to process more easily. Check-level scripts are able to access the outputs of all of their subordinate commands, allowing them to consolidate multiple items into a single recommendation. Generally, command-level scripts are handled by Xylok LLC, while check-level PP will eventually be available to customers.

Command-level Example

Say you are writing post-processing to check a system login banner matches the expected banner. You don’t want to fail a banner that’s “close enough” though, because the differences might simply be whitespace, punctuation, or similar items that don’t materially make a difference. The PP code below uses one of the built-in utility functions to calculate how far off the found banner is verses the expectation and the report that information as a status and comment.

Note

This is a “command-level” script, but a check level script works very similarly. The only real difference is that at the command level you directly access the data using ctx.raw (context.Context.raw), whereas a the check-level you get the data from the commands using context.Context.values(). See Check-Level PP.

# Notional comand that this PP works with
cat /etc/issue

# Example output for this:
We are watching you. be good
# utils.text is one of the provided utility modules
from utils.text import levenshtein

# Stand in for the standard DoD Banner to keep the code shorter
standardBanner = "We are watching you, be good."
bannerLength = len(standardBanner)

# ctx.raw uses the provided context and pulls the original command output out of it
bannerDiff = levenshtein(ctx.raw, standardBanner)
percentDiff = 1 - ((bannerLength - bannerDiff) / bannerLength)

# Anything printed to standard out will be displayed to the user in place of the 'original' command output
print(f"Banner: {ctx.raw}\n")
print(f"Difference (in distance): {bannerDiff})")

if percentDiff > 0.05:
    # This banner was too far off, so we recommend the check be non-compliant and give a reasonable comment
    ctx.recommend_noncompliant(f"The banner did not match the expected banner and was wrong by {percentDiff*100}%.")

    # We've made our choice, no need to do anything else!
    return

# This banner was close enough, so we recommend the check be compliant and give a reasonable comment
ctx.recommend_compliant("The system banner matches the expected banner.")

Output displayed in the “output” area of XSS:

Banner: We are watching you. be good
Difference (in distance): 3

Recommendations shown in the “PP recommends” area of XSS (and used by default when applying auto-analysis):

Recommended comment: The system banner watches the expected banner.
Recommended status: compliant

Script Wrapper

The script entered as a post-processing script gets dropped into a script that looks roughly like:

context = Context('stuff configured by the PP framework')

def the_actual_pp_script(ctx):
    # BEGIN CUSTOM CODE
    ############ YOUR CODE HERE ##################
    # END CUSTOM CODE

try:
    the_actual_pp_script(context)
except Exception as e:
    print('info about exception')

Postprocessing code should rely only on the arguments passed into the_actual_pp_script. Other details are subject to change.

Tip

Use early returns! Because your code is running inside a function, you can always return to end your script early. That means you can avoid deeply nested logic by using things like guard clauses.

Input

When your script is run, it is given raw output from the command and other metadata. Scripts should rely primarily on the ctx argument they receive, which is an subclass of context.Context. The context passed in is either a context.CommandValueContext (for command-level PP) or a context.CheckItemContext (for check-level PP).

For backwards compability, raw_output and questions are directly available, but context.Context.raw and context.Context.answer() should be prefered in new scripts.

Output

Your script should output the desired result to standard output, typically using print(). Standard error will also be captured, although Xylok may use that in the future to detect errors.

If the postprocessing script has enough information to definitively determine if the output indicates a finding or not, it can use context.Context.recommend_status(), context.Context.recommend_comment(), or the other helper recommendation functions to record that information.

If a script needs to exit early, a standard Python return is the best approach. os.exit() may result in unexpected behavior.

If uncaptured exceptions occur during your script’s execution, they will be displayed in the script output and the PP status will be marked as having thrown an exception.

Modules

By default, nothing is imported; this script should import standard library modules as needed. There are also a helper utilities modules and some third-party modules available. See Python Version and Modules for more information.

Check-Level PP

The Command-level Example showed a script that took in raw data to do its work. If a check doesn’t have post-processing, XSS internally uses a default script which consolidates all the command-level PP. Roughly, this scripts looks like this:

#print("Consolidating child recommendations")
for v in ctx.values():
    if v.has_exception():
        print("An issue occurred processing the command values, you may want to report this to Xylok support.")
        print("For now, manually review the raw output.")
        ctx.recommend_manual_review()
        return

output = ctx.consolidate_value_additional_outputs()
ctx.additional_output = output
# for k, v in output.items():
#     print(f"'{k}': '{v}'")

A few items to call out here:

  • Check-level contexts can use context.Context.values() to retrieve all the commands that ran for that check

  • If a check level script outputs data, it will be displayed within Xylok, but using print() is not required to provide recommendations. The default script doesn’t print anything (unless there’s an exception), but it still updates the check recommendations.

  • consolidate_value_additional_outputs() is provided to consistently find the “high water” recommended status and combined comment for all the child commands. You are free to use it within your own scripts as well. Notice that it does not update the check recommendations by default to allow for more flexibility. For this default script we just set our recommendations to whatever the conslidation determined.

More details on what is available to a check-level script can be found by reading the Context documentation. A few items to concentrate on:

  • context.Context.values() and value() take in an optional command tag. Tags are a way to identify particular commands without having to look at the specific command run. IE, a command might have the tag motd assigned if it’s a command that cats out /etc/issue. Tags assigned to commands can be found next to the command details on the XSS site. To find those, go to Reference->Benchmarks->find your benchmark->find your check->scroll to the Commands section (you must be logged in to see it). In addition to the displayed tags, all OSes/OS tags assigned to a command can be used to search values().

  • value() is often valuable because there’s only a single command run for a check. It will return the command directly (or None if not found).

Best Practices

  • Use the data and functions off ctx. Many older PP scripts use raw_output, recommend_XYZ, or answers globals, but these are deprecated and shouldn’t be used for new code.

  • Use early returns to avoid deeply nested if s and loops

  • For logic shared by several checks, try to consolidate into a shared utils function.

  • For local complex logic that isn’t shared, define functions within your script to help with readability.

  • Don’t complicate your logic by handling exceptions if all you’re doing is reporting the exception. The outer wrapper already does this for you. Still handle exceptions if there’s something you can do with it though, like catch a parsing error and try a different approach!

  • Use Python f-strings, they can by much clearer than concatening strings.

Python Version and Modules

Base Environment

Post-processing scripts run in a Python 3.14 environment within a container, with all of the standard library available. Things to keep in mind when developing scripts:

  • Never assume files on disk will last beyond the life of your script.

  • Scripts do not have access to the host file system.

  • XSS standard PP scripts will never use never use network resources.

  • Customer PP scripts may use network resources, but because the script is inside a container some networking details may change.

Utility Modules

The post-processor ships with a utils module that includes tools specific to the needs of PP scripts. These functions are documented in detail in the utils.

NetworkParse

The included utils.networkparse.parse.automatic() module helps with processing common network configuration formats for switches, routers, and firewalls. The NetworkParse Tutorial will help get you started.

Third-Party Modules

In addition to the Python standard library, the following modules are installed and available to post-processing scripts:

  • “bind9-parser>=0.9.8”,

  • “ciscoconfparse>=1.9.52”,

  • “dateparser>=1.2.2”,

  • “python-box>=7.3.2”,

  • “tabulate>=0.9.0”,

  • “terminaltables>=3.1.10”,

  • “uritemplate>=4.2.0”,

See their respective documentation for more information.