# -*- coding: utf-8 -*-
"""Constants for wizards."""
import re
from typing import List, Optional, Union
from .api import GUI_PAGE_SIZES
[docs]class Templates:
"""Query builder templates."""
LEFT: str = "({query}"
"""For building a query with a left parentheses"""
RIGHT: str = "{query})"
"""For building a query with a right parentheses"""
NOT: str = "not {query}"
"""For building a query with a NOT operator"""
OR: str = "or {query}"
"""For building a query with an OR operator"""
AND: str = "and {query}"
"""For building a query with an AND operator"""
COMPLEX: str = '("{field}" == match([{sub_queries}]))'
"""For building a query for complex fields"""
SUBS: str = " and "
"""Joiner for sub fields in a complex field"""
[docs]class Results:
"""Keys for results returned from wizards."""
EXPRS: str = "expressions"
"""key containing the expressions that the GUI query wizard can understand"""
QUERY: str = "query"
"""key containing the AQL produced by the parser"""
[docs]class Patterns:
"""Regular expression patterns for validation of values in entries."""
FIELD_VALID: str = re.compile(
r"""(?ix) # case insensitive and verbose
([^a-z0-9:._\-]) # contains characters that are not one of: a-z 0-9 : . _ -
""",
)
"""regex for validating that a field name is valid"""
FIELD_FIRST_ALPHA: str = re.compile(
r"""(?ix) # case insensitive and verbose
(^[^a-zA-Z]) # starts with characters that are not one of: a-z
"""
)
"""regex for validating that a field name begins with alpha characters"""
OP_ALPHA: str = re.compile(
r"""(?ix) # case insensitive and verbose
([^a-z0-9_\-]) # contains characters that are not one of: a-z 0-9 _ -
"""
)
"""regex for validating operators"""
FLAGS: str = re.compile(
r"""(?ix) # case insensitive and verbose
(?P<flags>[^a-z0-9]*)? # capture optional flags at beginning
(?P<value>.*) # capture the rest as the value
"""
)
"""regex for validating flags"""
FIELD: List[str] = [FIELD_VALID, FIELD_FIRST_ALPHA]
"""regex validators to use for fields"""
OP: List[str] = [OP_ALPHA]
"""regex validators to use for operators"""
[docs]class Flags:
"""Flag values that can be used in values for entries."""
NOT: str = "!"
"""Flag to NOT an entry"""
AND: str = "&"
"""Flag to AND an entry"""
OR: str = "|"
"""Flag to OR an entry"""
LEFTB: str = "("
"""Flag to start a parenthesis"""
RIGHTB: str = ")"
"""Flag to end a perenthesis, can also be at end of a value"""
FLAGS: dict = {
AND: "Use and instead of or (default)",
OR: f"Use or instead of and (overrides {AND})",
NOT: "Use not",
LEFTB: "Open a parentheses",
RIGHTB: "Close a parentheses (can also be at end of entry)",
}
"""valid flags and their descriptions"""
LFMT: str = "[" + " ".join(list(FLAGS)) + "]"
"""Doc string to show the flags that can be at the beginning of a value"""
RFMT: str = f"[{RIGHTB}]"
"""Doc string to show the flags that can be at the end of a value"""
FMT_TEXT: str = "\n# " + "\n# ".join([f"{k} {v}" for k, v in FLAGS.items()])
FMT_CSV: str = ", ".join([f"{k} {v}" for k, v in FLAGS.items()])
[docs]class Entry:
"""Entry keys and split values."""
SRC: str = "source"
"""key of the str of where an entry came from"""
WEIGHT: str = "bracket_weight"
"""key of the bracket weight for levels of filters within parenthesis"""
FLAGS: str = "flags"
"""key of the flags were supplied in the value for an entry"""
VALUE: str = "value"
"""key of the value for the entry"""
TYPE: str = "type"
"""type of entry"""
REQ: List[str] = [VALUE, TYPE]
"""Required keys for entries"""
SPLIT: str = " "
"""String to split on for expressions"""
CSPLIT: str = " // "
"""String to split on for complex expressions"""
[docs]class EntrySq:
"""Entry keys for saved query types."""
NAME: str = "name"
"""key of name of SQ"""
DESC: str = "description"
"""key of description of SQ"""
TAGS: str = "tags"
"""key of tags of SQ"""
FIELDS: str = "fields"
"""key of fields for SQ"""
DEFAULT: str = "default"
"""if found in FIELDS column, insert asset API object default fields"""
FDEF: str = "fields_default"
FMAN: str = "fields_manual"
PRIVATE: str = "private"
ASSET_SCOPE: str = "asset_scope"
ALWAYS_CACHED: str = "always_cached"
PAGE_SIZE: str = "gui_page_size"
REQ: List[str] = [*Entry.REQ]
"""Required keys for saved query types"""
OPT_ENTRY: dict = {
PRIVATE: False,
ALWAYS_CACHED: False,
ASSET_SCOPE: False,
PAGE_SIZE: GUI_PAGE_SIZES[0],
}
OPT: dict = {
DESC: "",
TAGS: "",
FIELDS: DEFAULT,
**OPT_ENTRY,
}
"""Optional keys and their defaults for saved query types"""
BOOLS: List[str] = [PRIVATE, ASSET_SCOPE, ALWAYS_CACHED]
INTS: List[str] = [PAGE_SIZE]
[docs]class Types:
"""Types of entries."""
SIMPLE: str = "simple"
"""simple entry type, similar to 'Aggregated Data' GUI query wizard type"""
COMPLEX: str = "complex"
"""complex entry type, similar to 'Complex Field' GUI query wizard type"""
SAVED_QUERY: str = "saved_query"
"""saved query type, used in CSV wizard"""
FILE: str = "file"
"""file type, used in CLI wizards"""
LINES: str = "lines"
"""lines type, used in CLI wizards"""
DICT: List[str] = [SIMPLE, COMPLEX]
"""valid types for the base Wizard class."""
TEXT: List[str] = [*DICT]
"""valid types for the WizardText class."""
SQ: List[str] = [*DICT, SAVED_QUERY]
"""valid types for the WizardCsv class."""
CLI: List[str] = [*DICT, FILE, LINES]
"""valid types for the CLI."""
[docs]class Docs:
"""Documentation strings for wizards."""
SUB_OPT: str = f"[{Entry.CSPLIT} ...]"
OPVAL: str = "FIELD OPERATOR VALUE"
FMT_SIMPLE: str = f"{Flags.LFMT} {OPVAL} {Flags.RFMT}"
FMT_COMPLEX: str = f"{Flags.LFMT} COMPLEX-FIELD{Entry.CSPLIT}SUB-{OPVAL}{SUB_OPT} {Flags.RFMT}"
DESC_SIMPLE: str = "Filter entry for simple fields"
DESC_COMPLEX: str = "Filter entry for complex fields and their sub-fields"
EX_SIMPLE1: str = f"{Flags.LEFTB} hostname contains test"
EX_SIMPLE2: str = f"{Flags.NOT} hostname contains internal {Flags.RIGHTB}"
EX_SIMPLE3: str = f"{Flags.LEFTB} os.type equals windows"
EX_SIMPLE4: str = f"{Flags.OR} os.type equals os x {Flags.RIGHTB}"
EX_COMPLEX1: str = (
f"installed_software{Entry.CSPLIT}name contains chrome"
f"{Entry.CSPLIT}version earlier_than 82"
)
EX_TEXT: str = f"""{Types.SIMPLE:<8} {EX_SIMPLE1}
{Types.SIMPLE:<8} {EX_SIMPLE2}
{Types.SIMPLE:<8} {EX_SIMPLE3}
{Types.SIMPLE:<8} {EX_SIMPLE4}
{Types.COMPLEX:<8} {EX_COMPLEX1}
"""
EX_DICT: str = f"""[
{{
"{Entry.TYPE}": "{Types.SIMPLE}",
"{Entry.VALUE}": "{EX_SIMPLE1}"
}},
{{
"{Entry.TYPE}": "{Types.SIMPLE}",
"{Entry.VALUE}": "{EX_SIMPLE2}"
}},
{{
"{Entry.TYPE}": "{Types.SIMPLE}",
"{Entry.VALUE}": "{EX_SIMPLE3}"
}},
{{
"{Entry.TYPE}": "{Types.SIMPLE}",
"{Entry.VALUE}": "{EX_SIMPLE4}"
}},
{{
"{Entry.TYPE}": "{Types.COMPLEX}",
"{Entry.VALUE}": "{EX_COMPLEX1}"
}}
]
"""
EX_FIELDS: str = "os.distribution,os.os_str,active_directory:ad_password_last_set"
GUI_PAGE_SIZES_STR = " or ".join([str(x) for x in GUI_PAGE_SIZES]) + " (default 20 if empty)"
OPT_BOOL = "Optional: True or False (default False if empty)"
EX_CSV: str = f"""
{Entry.TYPE},{Entry.VALUE},{EntrySq.DESC},{EntrySq.TAGS},{EntrySq.FIELDS},{EntrySq.ASSET_SCOPE},{EntrySq.PRIVATE},{EntrySq.ALWAYS_CACHED},{EntrySq.PAGE_SIZE}
"# If {Entry.TYPE} column is empty or begins with # it is ignored",,,,
"# {Entry.TYPE} of {Types.SIMPLE} or {Types.COMPLEX} will belong to the {Types.SAVED_QUERY} they are under",,,,
"# Column descriptions for {Entry.TYPE} of {Types.SAVED_QUERY}","Name of Saved Query","Description of Saved Query","Tags to apply to Saved Query","Columns to display in Saved Query",{OPT_BOOL},{OPT_BOOL},{OPT_BOOL},"{GUI_PAGE_SIZES_STR}",
"# Column descriptions for {Entry.TYPE} of {Types.SIMPLE}","Format -- [] represents optional items: {FMT_SIMPLE}","Description: {DESC_SIMPLE}","Only uses columns {Entry.TYPE} and {Entry.VALUE}",
"# Column descriptions for {Entry.TYPE} of {Types.COMPLEX}","Format -- [] represents optional items: {FMT_COMPLEX}","Description: {DESC_COMPLEX}","Only uses columns {Entry.TYPE} and {Entry.VALUE}",
"# Value Flags for {Entry.TYPE} of {Types.SIMPLE} or {Types.COMPLEX}","{Flags.FMT_CSV}",,,
"{Types.SAVED_QUERY}","example 1","Filters, default fields, custom fields","example,tag1,tag2","{EX_FIELDS},{EntrySq.DEFAULT},os.build"
"{Types.SIMPLE}","{EX_SIMPLE1}",,,
"{Types.SIMPLE}","{EX_SIMPLE2}",,,
"{Types.SIMPLE}","{EX_SIMPLE3}",,,
"{Types.SIMPLE}","{EX_SIMPLE4}",,,
"{Types.SAVED_QUERY}","example 2","No filters, no default fields, custom fields","example,tag3,tag4","{EX_FIELDS}"
"{Types.SAVED_QUERY}","example 3","No filters, default fields, no custom fields","example,tag5,tag6",
""" # noqa: E501
TEXT: str = f"""
# Example:
{EX_TEXT}
# Format -- [] represents optional items:
{Types.SIMPLE:<8} {FMT_SIMPLE}
# Description: {DESC_SIMPLE}
{Types.COMPLEX:<8} {FMT_COMPLEX}
# Description: {DESC_COMPLEX}
# Flags:{Flags.FMT_TEXT}
"""
DICT: str = f"""
# Example:
{EX_DICT}
# Format -- [] represents optional items:
# "{Entry.TYPE}": "{Types.SIMPLE}, "{Entry.VALUE}": "{FMT_SIMPLE}"
# Description: "{DESC_SIMPLE}"
# "{Entry.TYPE}": "{Types.COMPLEX}", "{Entry.VALUE}": "{FMT_COMPLEX}"
# Description: "{DESC_COMPLEX}"
# Flags:{Flags.FMT_TEXT}
"""
CSV: str = f"{EX_CSV}"
[docs]class Sources:
"""Defaults for wizard source argument."""
CSV_STR: str = "csv text string"
CSV_PATH: str = "csv file {path}"
TEXT_STR: str = "text string"
TEXT_PATH: str = "text file {path}"
LOD: str = "list of dictionaries"
JSON_STR: str = "json string"
JSON_PATH: str = "json file {path}"
[docs]class Fields:
"""Keys and arguments for field schemas."""
NAME: str = "name"
EXPR_TYPE: str = "expr_field_type"
ANAME: str = "adapter_name"
SUBS: str = "sub_fields"
IS_ALL: str = "is_all"
IS_DETAILS: str = "is_details"
IS_COMPLEX: str = "is_complex"
[docs]class Expr:
"""Keys for GUI expressions."""
BRACKET_LEFT: str = "leftBracket"
BRACKET_RIGHT: str = "rightBracket"
BRACKET_WEIGHT: str = "bracketWeight"
CHILDREN: str = "children"
CONDITION: str = "condition"
CONTEXT: str = "context"
EXPR: str = "expression"
FIELD: str = "field"
FIELD_TYPE: str = "fieldType"
FILTER: str = "filter"
FILTER_ADAPTERS: str = "filteredAdapters"
IDX: str = "i"
NOT: str = "not"
OP_COMP: str = "compOp"
OP_LOGIC: str = "logicOp"
VALUE: str = "value"
CONTEXT_OBJ: str = "OBJ"
OP_AND: str = "and"
OP_OR: str = "or"
OP_IDX0: str = ""
[docs] @classmethod
def get_query(cls, exprs: List[dict]) -> str:
"""Get the query for a list of GUI expressions.
Args:
exprs: list of expressions to build query from
"""
return " ".join([x[cls.FILTER] for x in exprs])
[docs] @classmethod
def get_subs_query(cls, sub_exprs: List[dict]) -> str:
"""Get the complex query for a list of GUI child expressions.
Args:
sub_exprs: list of children of a complex expression to build query from
"""
return Templates.SUBS.join([x[cls.CONDITION] for x in sub_exprs])
[docs] @classmethod
def build(
cls,
entry: dict,
query: str,
field: dict,
idx: int,
op_comp: str,
field_name_override: Optional[str] = None,
value: Optional[Union[int, str, bool]] = None,
is_complex: bool = False,
children: Optional[List[dict]] = None,
) -> dict:
"""Build an expression for the GUI to understand the query.
Args:
entry: entry to build expression from
query: AQL string
field: schema of field
idx: index of this expression
field_name_override: value to use as name of field in expr instead of 'name' key from
field dict
value: raw expression value
op_comp: comparison operator
is_complex: build an expression for a complex filter
children: children of a complex filter
"""
flags = entry.get(Entry.FLAGS, []) or []
weight = entry.get(Entry.WEIGHT, 0)
is_right = Flags.RIGHTB in flags
is_left = Flags.LEFTB in flags
is_not = Flags.NOT in flags
is_or = Flags.OR in flags
if is_not:
query = Templates.NOT.format(query=query)
if is_right:
query = Templates.RIGHT.format(query=query)
if is_left:
query = Templates.LEFT.format(query=query)
if idx:
if is_or:
query = Templates.OR.format(query=query)
op_logic = cls.OP_OR
else:
query = Templates.AND.format(query=query)
op_logic = cls.OP_AND
else:
op_logic = cls.OP_IDX0
field_name = (
field_name_override if isinstance(field_name_override, str) else field[Fields.NAME]
)
expression = {}
expression[cls.BRACKET_WEIGHT] = weight
expression[cls.CHILDREN] = children or [cls.build_child()]
expression[cls.OP_COMP] = op_comp
expression[cls.FIELD] = field_name
expression[cls.FIELD_TYPE] = field[Fields.EXPR_TYPE]
expression[cls.FILTER] = query
expression[cls.FILTER_ADAPTERS] = None
expression[cls.BRACKET_LEFT] = is_left
expression[cls.OP_LOGIC] = op_logic
expression[cls.NOT] = is_not
expression[cls.BRACKET_RIGHT] = is_right
expression[cls.VALUE] = value
if idx:
expression[cls.IDX] = idx
if is_complex:
expression[cls.CONTEXT] = cls.CONTEXT_OBJ
return expression
[docs] @classmethod
def build_child(
cls,
query: str = "",
op_comp: str = "",
field: str = "",
value: Optional[Union[int, str, bool]] = None,
idx: int = 0,
) -> dict:
"""Build a child expression to be used in an expression.
Args:
query: AQL of this child expression
op_comp: comparison operator
field: name of field for this child
value: raw expression value
idx: index of this expression
"""
expression = {}
expression[cls.CONDITION] = query
expression[cls.EXPR] = {}
expression[cls.EXPR][cls.OP_COMP] = op_comp
expression[cls.EXPR][cls.FIELD] = field
expression[cls.EXPR][cls.FILTER_ADAPTERS] = None
expression[cls.EXPR][cls.VALUE] = value
expression[cls.IDX] = idx
return expression