From 509b2a35d2f9e85d71452f31812f5d4630bc8cd3 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 5 May 2026 15:07:05 +1200 Subject: [PATCH 1/6] initial newlistrules.py Assisted-by: Microsoft Copilot --- newlistrules.py | 267 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 newlistrules.py diff --git a/newlistrules.py b/newlistrules.py new file mode 100644 index 0000000..c0dd667 --- /dev/null +++ b/newlistrules.py @@ -0,0 +1,267 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# listrules.py +# May 2026 +# +# Change History +# May 2026 - Initial release +# +# Copyright © 2026, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Assisted-by: Microsoft Copilot + +import argparse +import datetime +import json +import sys +from typing import List, Optional + +from sharedfunctions import callrestapi, printresult + + +# --- Configuration --- +LIMITVAL = 10000 +DESIRED_OUTPUT_COLUMNS = ['objectUri','containerUri','principalType','principal','setting','permissions','description','reason','createdBy','createdTimestamp','modifiedBy','modifiedTimestamp','condition','matchParams','mediaType','enabled','version','id'] +VALID_PERMISSIONS = {'read','update','delete','secure','add','remove','create'} +PRINCIPAL_TYPES = {'guest','everyone','authenticatedUsers'} + + +# --- Helpers --- +def parse_bool(val: str) -> Optional[bool]: + if val is None or val.lower() in ('none',''): + return None + v = val.lower() + if v in ('true','t','1','yes','y'): + return True + if v in ('false','f','0','no','n'): + return False + raise argparse.ArgumentTypeError(f"Invalid boolean value: {val}") + +def parse_date(val: str) -> str: + # Accept ISO 8601 date or datetime; raise on invalid + try: + # allow date-only or datetime + if 'T' in val: + datetime.datetime.fromisoformat(val.replace('Z','+00:00')) + else: + datetime.date.fromisoformat(val) + return val + except Exception: + raise argparse.ArgumentTypeError(f"Invalid date/datetime: {val}. Use ISO 8601 format.") + +def join_or(expressions: List[str]) -> str: + expressions = [e for e in expressions if e] + if not expressions: + return '' + if len(expressions) == 1: + return expressions[0] + return 'or(' + ','.join(expressions) + ')' + +def join_and(expressions: List[str]) -> str: + expressions = [e for e in expressions if e] + if not expressions: + return '' + if len(expressions) == 1: + return expressions[0] + return 'and(' + ','.join(expressions) + ')' + +def contains(field: str, value: str) -> str: + return f"contains({field},'{value}')" + +def cmp(field: str, value: str, operator: str = 'eq') -> str: + op = operator.lower() + if op not in ('eq','ne','contains'): + raise ValueError("Unsupported operator: must be 'eq', 'ne' or 'contains") + return f"{op}({field},'{value}')" + +def cmpdefault(field: str, value: str, operator: str = 'eq') -> str: + op = 'eq' + return f"{op}({field},'{value}')" + +def in_list(field: str, values: List[str], operator: str = 'eq') -> str: + # build or(eq(field,val1),eq(field,val2),...) or or(ne(...)) + return join_or([cmp(field, v, operator) for v in values]) + + +# --- Build filter expression --- +def build_filter(args) -> str: + clauses = [] + operator = args.operator.lower() if args.operator else 'eq' + + # objectUri contains + if args.uri and args.uri.lower() != 'none': + clauses.append(contains('objectUri', args.uri)) + + # containerUri contains + if args.container and args.container.lower() != 'none': + clauses.append(contains('containerUri', args.container)) + + # description contains (CASE SENSITIVE as original) + if args.description and args.description != 'none': + clauses.append(contains('description', args.description)) + + # principal or principalType (use cmp for eq/ne) + if args.principal and args.principal.lower() != 'none': + p = args.principal + if p.lower() == 'authenticatedusers': + p = 'authenticatedUsers' + if p in PRINCIPAL_TYPES: + clauses.append(cmp('principalType', p, operator)) + else: + clauses.append(cmp('principal', p, operator)) + + # enabled boolean (use cmp if operator is 'eq' or 'ne') + if args.enabled and args.enabled.lower() != 'none': + b = parse_bool(args.enabled) + if b is not None and operator in ['eq','ne']: + clauses.append(cmp('enabled', 'true' if b else 'false', operator)) + else: + clauses.append(cmpdefault('enabled', 'true' if b else 'false', operator)) + + # status list (alias for setting or custom) + if args.status: + clauses.append(in_list('setting', args.status, operator)) + + # permissions: ensure valid and build contains/any logic + if args.permission: + invalid = [p for p in args.permission if p not in VALID_PERMISSIONS] + if invalid: + raise SystemExit(f"Invalid permission(s): {invalid}. Valid: {sorted(VALID_PERMISSIONS)}") + # permissions are matched with contains; operator doesn't apply to contains + perm_clauses = [contains('permissions', p) for p in args.permission] + clauses.append(join_or(perm_clauses)) + + # created/modified ranges (these are range ops and unaffected by eq/ne) + if args.created_after: + clauses.append(f"gt(createdTimestamp,'{args.created_after}')") + if args.created_before: + clauses.append(f"lt(createdTimestamp,'{args.created_before}')") + if args.modified_after: + clauses.append(f"gt(modifiedTimestamp,'{args.modified_after}')") + if args.modified_before: + clauses.append(f"lt(modifiedTimestamp,'{args.modified_before}')") + + # mediaType exact match (use cmp) + if args.media_type and args.media_type.lower() != 'none': + clauses.append(cmp('mediaType', args.media_type, operator)) + + # condition or matchParams free text contains + if args.condition: + clauses.append(contains('condition', args.condition)) + if args.match_params: + clauses.append(contains('matchParams', args.match_params)) + + # custom raw filter (allow advanced users to pass a full filter expression) + if args.raw_filter: + clauses.append(args.raw_filter) + + # combine all clauses with and + return join_and(clauses) + + +# --- Argument parsing --- +parser = argparse.ArgumentParser(description="List rules for a principal and/or an endpoint with flexible filters") + +parser.add_argument("-u","--uri", help="String that objectUri contains", default="none") +parser.add_argument("-c","--container", help="String that containerUri contains", default="none") +parser.add_argument("-p","--principal", help="Identity name (id) or authenticatedUsers, everyone or guest", default='none') +parser.add_argument("-d","--description", help="String that's included in a rule description (CASE SENSITIVE)", default='none') +parser.add_argument("-e","--enabled", help="Status of rules to return true or false", default='none') +parser.add_argument("--operator", help="Comparison operator to use for equality filters", choices=['eq','ne','contains'], default='eq') +parser.add_argument("--condition", help="Filter by condition contains text") +parser.add_argument("--media-type", dest='media_type', help="Filter by mediaType") +#parser.add_argument("--perms", help="Sting of permissions to search for in the rules", default="none") +#parser.add_argument("--permission", action='append', help="Filter by permission. Can be repeated", metavar='PERM') +parser.add_argument("--created-after", dest='created_after', type=parse_date, help="Created after ISO8601 datetime") +parser.add_argument("--created-before", dest='created_before', type=parse_date, help="Created before ISO8601 datetime") +parser.add_argument("--modified-after", dest='modified_after', type=parse_date, help="Modified after ISO8601 datetime") +parser.add_argument("--modified-before", dest='modified_before', type=parse_date, help="Modified before ISO8601 datetime") +#parser.add_argument("--match-params", dest='match_params', help="Filter by matchParams contains text") +parser.add_argument("--raw-filter", help="Raw filter expression to append (advanced users)") +parser.add_argument("--csvheader", help="(Optional) Disables header - must be combined with '-o csv'", choices=['yes','no'], default='yes') +parser.add_argument("-o","--output", help="Output Style", choices=['csv','json','simple','simplejson'], default='json') + +args = parser.parse_args() + +# --- Build request path --- +try: + filter_expr = build_filter(args) +except SystemExit as e: + print(str(e), file=sys.stderr) + sys.exit(2) +except ValueError as e: + print(str(e), file=sys.stderr) + sys.exit(2) + +base = "/authorization/rules" +if filter_expr: + reqval = f"{base}?filter={filter_expr}&limit={LIMITVAL}" +else: + reqval = f"{base}?limit={LIMITVAL}" + +reqtype = 'get' + +# make the rest call +rules_result_json = callrestapi(reqval, reqtype) + + +#print the result if output style is json or simple +if args.output in ['json','simple']: + printresult(rules_result_json,args.output) +elif args.output=='csv': + if args.csvheader=='yes': + # Print a header row + print(','.join(map(str,DESIRED_OUTPUT_COLUMNS))) + else: + pass + if 'items' in rules_result_json: + for item in rules_result_json['items']: + outstr='' + for column in DESIRED_OUTPUT_COLUMNS: + # Add a comma to the output string, even if we will not output anything else, unless this is the very first desired output column + if column is not DESIRED_OUTPUT_COLUMNS[0]: outstr=outstr+',' + if column=='setting': + # The setting value is derived from two columns: type and condition. + if 'condition' in item: + #print("Condition found") + outstr=outstr+'conditional '+item['type'] + else: + outstr=outstr+item['type'] + elif column in item: + # This column is in the results item for this rule + # Most columns are straight strings, but a few need special handling + if column in ['condition','description','reason']: + # The these strings can have values whcih contain commas, need we to quote them to avoid the commas being interpreted as column separators in the CSV + outstr=outstr+'"'+item[column]+'"' + elif column=='permissions': + # Construct a string listing each permission in the correct order, separated by spaces and surrounded by square brackets + outstr=outstr+'[' + permstr='' + # Output permissions in the order we choose, not the order they appear in the result item + for permission in VALID_PERMISSIONS: + for result_permission in item['permissions']: + if permission == result_permission: + # Add a space to separate permissions if this isn't the first permission + if not permstr=='': permstr=permstr+' ' + permstr=permstr+result_permission + outstr=outstr+permstr+']' + else: + # Normal column + # Some columns contain non-string values: matchParams and enabled are boolean, version is integer. Convert everything to a string. + outstr=outstr+str(item[column]) + print(outstr) +else: + print ("output_style can be json, simple or csv. You specified " + args.output + " which is invalid.") From 7ebce128069f4168c0781bb5c093d227a38bcb36 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 5 May 2026 15:32:14 +1200 Subject: [PATCH 2/6] code trim and bugfix Trimmed down some of the code that was surplus and added if statement to handle special user (principalTypes) when used in combination with unsupported operator types. --- newlistrules.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/newlistrules.py b/newlistrules.py index c0dd667..a94fc8b 100644 --- a/newlistrules.py +++ b/newlistrules.py @@ -78,9 +78,6 @@ def join_and(expressions: List[str]) -> str: return expressions[0] return 'and(' + ','.join(expressions) + ')' -def contains(field: str, value: str) -> str: - return f"contains({field},'{value}')" - def cmp(field: str, value: str, operator: str = 'eq') -> str: op = operator.lower() if op not in ('eq','ne','contains'): @@ -119,7 +116,10 @@ def build_filter(args) -> str: if p.lower() == 'authenticatedusers': p = 'authenticatedUsers' if p in PRINCIPAL_TYPES: - clauses.append(cmp('principalType', p, operator)) + if operator in ['eq','ne']: + clauses.append(cmp('principalType', p, operator)) + else: + clauses.append(cmpdefault('principalType', p, operator)) else: clauses.append(cmp('principal', p, operator)) @@ -131,10 +131,7 @@ def build_filter(args) -> str: else: clauses.append(cmpdefault('enabled', 'true' if b else 'false', operator)) - # status list (alias for setting or custom) - if args.status: - clauses.append(in_list('setting', args.status, operator)) - + ''' YET TO BE DEVELOPED # permissions: ensure valid and build contains/any logic if args.permission: invalid = [p for p in args.permission if p not in VALID_PERMISSIONS] @@ -143,6 +140,7 @@ def build_filter(args) -> str: # permissions are matched with contains; operator doesn't apply to contains perm_clauses = [contains('permissions', p) for p in args.permission] clauses.append(join_or(perm_clauses)) + ''' # created/modified ranges (these are range ops and unaffected by eq/ne) if args.created_after: @@ -158,12 +156,15 @@ def build_filter(args) -> str: if args.media_type and args.media_type.lower() != 'none': clauses.append(cmp('mediaType', args.media_type, operator)) - # condition or matchParams free text contains + # condition free text contains if args.condition: clauses.append(contains('condition', args.condition)) + + ''' YET TO BE DEVELOPED if args.match_params: clauses.append(contains('matchParams', args.match_params)) - + ''' + # custom raw filter (allow advanced users to pass a full filter expression) if args.raw_filter: clauses.append(args.raw_filter) From 3f6bcb60c9c6f6d30d98e761ad56b254efbb0415 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 5 May 2026 16:57:58 +1200 Subject: [PATCH 3/6] further enhancements and bugfixes --- newlistrules.py | 53 +++++++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/newlistrules.py b/newlistrules.py index a94fc8b..6d19d29 100644 --- a/newlistrules.py +++ b/newlistrules.py @@ -44,9 +44,9 @@ def parse_bool(val: str) -> Optional[bool]: if val is None or val.lower() in ('none',''): return None v = val.lower() - if v in ('true','t','1','yes','y'): + if v in ('true','t','1','yes','y','enabled'): return True - if v in ('false','f','0','no','n'): + if v in ('false','f','0','no','n','disabled'): return False raise argparse.ArgumentTypeError(f"Invalid boolean value: {val}") @@ -78,6 +78,9 @@ def join_and(expressions: List[str]) -> str: return expressions[0] return 'and(' + ','.join(expressions) + ')' +def contains(field: str, value: str) -> str: + return f"contains({field},'{value}')" + def cmp(field: str, value: str, operator: str = 'eq') -> str: op = operator.lower() if op not in ('eq','ne','contains'): @@ -98,13 +101,13 @@ def build_filter(args) -> str: clauses = [] operator = args.operator.lower() if args.operator else 'eq' - # objectUri contains + # objectUri (use cmp) if args.uri and args.uri.lower() != 'none': - clauses.append(contains('objectUri', args.uri)) + clauses.append(cmp('objectUri', args.uri, operator)) - # containerUri contains + # containerUri cmp if args.container and args.container.lower() != 'none': - clauses.append(contains('containerUri', args.container)) + clauses.append(cmp('containerUri', args.container, operator)) # description contains (CASE SENSITIVE as original) if args.description and args.description != 'none': @@ -156,10 +159,11 @@ def build_filter(args) -> str: if args.media_type and args.media_type.lower() != 'none': clauses.append(cmp('mediaType', args.media_type, operator)) - # condition free text contains + # condition free text (use contains) if args.condition: clauses.append(contains('condition', args.condition)) - +# print(clauses) + ''' YET TO BE DEVELOPED if args.match_params: clauses.append(contains('matchParams', args.match_params)) @@ -174,26 +178,27 @@ def build_filter(args) -> str: # --- Argument parsing --- -parser = argparse.ArgumentParser(description="List rules for a principal and/or an endpoint with flexible filters") - -parser.add_argument("-u","--uri", help="String that objectUri contains", default="none") -parser.add_argument("-c","--container", help="String that containerUri contains", default="none") -parser.add_argument("-p","--principal", help="Identity name (id) or authenticatedUsers, everyone or guest", default='none') -parser.add_argument("-d","--description", help="String that's included in a rule description (CASE SENSITIVE)", default='none') -parser.add_argument("-e","--enabled", help="Status of rules to return true or false", default='none') -parser.add_argument("--operator", help="Comparison operator to use for equality filters", choices=['eq','ne','contains'], default='eq') -parser.add_argument("--condition", help="Filter by condition contains text") -parser.add_argument("--media-type", dest='media_type', help="Filter by mediaType") +parser = argparse.ArgumentParser(description="newlistrules.py functions") + +parser.add_argument("-u","--uri", help="objectUri search, can be used with operator", default="none") +parser.add_argument("-c","--container", help="containerUri search, can be used with operator", default="none") +parser.add_argument("-p","--principal", help="Principal/Group ID, or 'authenticatedUsers', 'everyone' or 'guest'", default='none') +parser.add_argument("-d","--description", help="description search (contains only and CASE SENSITIVE)", default='none') +parser.add_argument("--condition", help="condition search (contains only and CASE SENSITIVE)") +parser.add_argument("--media-type", dest='media_type', help="mediaType search, can be used with operator") +parser.add_argument("-e","--enabled", help="Show rules enabled/true or disabled/false", default='none') +parser.add_argument("--operator", help="Filter operator", choices=['eq','ne','contains'], default='eq') +parser.add_argument("-o","--output", help="Output Style", choices=['csv','json','simple','simplejson'], default='json') +parser.add_argument("--csvheader", help="(Optional) Disables header - must be combined with '-o csv'", choices=['yes','no'], default='yes') #parser.add_argument("--perms", help="Sting of permissions to search for in the rules", default="none") #parser.add_argument("--permission", action='append', help="Filter by permission. Can be repeated", metavar='PERM') -parser.add_argument("--created-after", dest='created_after', type=parse_date, help="Created after ISO8601 datetime") -parser.add_argument("--created-before", dest='created_before', type=parse_date, help="Created before ISO8601 datetime") -parser.add_argument("--modified-after", dest='modified_after', type=parse_date, help="Modified after ISO8601 datetime") -parser.add_argument("--modified-before", dest='modified_before', type=parse_date, help="Modified before ISO8601 datetime") +parser.add_argument("--created-after", dest='created_after', type=parse_date, help="Created after datetime, e.g. 2026-01-01T00:00:00Z") +parser.add_argument("--created-before", dest='created_before', type=parse_date, help="Created before datetime, e.g. 2026-01-01T00:00:00Z") +parser.add_argument("--modified-after", dest='modified_after', type=parse_date, help="Modified after datetime, e.g. 2026-01-01T00:00:00Z") +parser.add_argument("--modified-before", dest='modified_before', type=parse_date, help="Modified before datetime, e.g. 2026-01-01T00:00:00Z") #parser.add_argument("--match-params", dest='match_params', help="Filter by matchParams contains text") parser.add_argument("--raw-filter", help="Raw filter expression to append (advanced users)") -parser.add_argument("--csvheader", help="(Optional) Disables header - must be combined with '-o csv'", choices=['yes','no'], default='yes') -parser.add_argument("-o","--output", help="Output Style", choices=['csv','json','simple','simplejson'], default='json') + args = parser.parse_args() From 5029cb83603eb721b07c828606b312410dbacc48 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 6 May 2026 16:20:15 +1200 Subject: [PATCH 4/6] new features and bugfixes Changes: Removed the original 'listrules.py' from this branch. Renamed 'newlistrules.py' to 'listrules.py'. Changed csv 'csvheader' option to 'headeroff' and set the default to including a header to ensure the same default behaviour as listrules.py v1.0 was persisted. Changed VALID_PERMISSIONS from being a set to a list to ensure the permissions order returned is consistent. Renamed columns and results for: createdTimestamp --> creationTimeStamp modifiedTimestamp --> modifiedTimeStamp New features: Option to print filter and REST call without submitting Option to filter by createdBy and modifiedBy Bugfix: Fixed a bug (through the column and result renamed) affecting the created* and modified* timestamp filters didn't work. --- listrules.py | 283 ++++++++++++++++++++++++++++++++++++++---------- newlistrules.py | 273 ---------------------------------------------- 2 files changed, 223 insertions(+), 333 deletions(-) delete mode 100644 newlistrules.py diff --git a/listrules.py b/listrules.py index 4ac733c..5e4fb61 100755 --- a/listrules.py +++ b/listrules.py @@ -2,15 +2,12 @@ # -*- coding: utf-8 -*- # # listrules.py -# August 2018 -# -# listrules +# May 2026 # # Change History -# December 2018 - Added custom CSV output code, which writes out consistent columns in a specific order for the result rules JSON -# January 2019 - Added 'id' to list of desired output columns +# May 2026 - Initial release # -# Copyright © 2018, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. +# Copyright © 2026, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the License); # you may not use this file except in compliance with the License. @@ -23,73 +20,240 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -# +# +# Assisted-by: Microsoft Copilot import argparse +import datetime +import json +import sys +from typing import List, Optional -from sharedfunctions import callrestapi, printresult +from sharedfunctions import callrestapi, printresult, getbaseurl -# setup command-line arguements -parser = argparse.ArgumentParser(description="List rules for a principal and/or an endpoint") -parser.add_argument("-u","--uri", help="Enter a string that the objecturi contains.",default="none") -parser.add_argument("-p","--principal", help="Enter the identity name or authenticatedUsers, everyone or guest",default='none') -parser.add_argument("-o","--output", help="Output Style", choices=['csv','json','simple','simplejson'],default='json') +# --- Configuration --- +LIMITVAL = 10000 +DESIRED_OUTPUT_COLUMNS = ['objectUri','containerUri','principalType','principal','setting','permissions','description','reason','createdBy','creationTimeStamp','modifiedBy','modifiedTimeStamp','condition','matchParams','mediaType','enabled','version','id'] +VALID_PERMISSIONS = ['read','update','delete','secure','add','remove','create'] +PRINCIPAL_TYPES = {'guest','everyone','authenticatedUsers'} -args = parser.parse_args() -objuri=args.uri -ident=args.principal -output_style=args.output - -# set the limit high so that all data is returned -limitval=10000 - -# Define columns we want to output for each rule item (whether the item has a value for that column or not) -desired_output_columns=['objectUri','containerUri','principalType','principal','setting','permissions','description','reason','createdBy','createdTimestamp','modifiedBy','modifiedTimestamp','condition','matchParams','mediaType','enabled','version','id'] -valid_permissions=['read','update','delete','secure','add','remove','create'] - -# build the request depending on what options were passed in -if ident.lower()=='authenticatedusers': ident='authenticatedUsers' - -if ident=='none' and objuri=='none': reqval= "/authorization/rules" -elif ident=='none' and objuri != 'none': reqval= "/authorization/rules?filter=contains(objectUri,'"+objuri+"')" -elif ident!='none' and objuri == 'none': - if ident=='guest' or ident=='everyone' or ident=='authenticatedUsers': - reqval= "/authorization/rules?filter=eq(principalType,'"+ident+"')" - else: - reqval= "/authorization/rules?filter=eq(principal,'"+ident+"')" -elif ident!='none' and objuri != 'none': + +# --- Helpers --- +def parse_bool(val: str) -> Optional[bool]: + if val is None or val.lower() in ('none',''): + return None + v = val.lower() + if v in ('true','t','1','yes','y','enabled'): + return True + if v in ('false','f','0','no','n','disabled'): + return False + raise argparse.ArgumentTypeError(f"Invalid boolean value: {val}") + +def parse_date(val: str) -> str: + # Accept ISO 8601 date or datetime; raise on invalid + try: + # allow date-only or datetime + if 'T' in val: + datetime.datetime.fromisoformat(val.replace('Z','+00:00')) + else: + datetime.date.fromisoformat(val) + return val + except Exception: + raise argparse.ArgumentTypeError(f"Invalid date/datetime: {val}. Use ISO 8601 format.") + +def join_or(expressions: List[str]) -> str: + expressions = [e for e in expressions if e] + if not expressions: + return '' + if len(expressions) == 1: + return expressions[0] + return 'or(' + ','.join(expressions) + ')' + +def join_and(expressions: List[str]) -> str: + expressions = [e for e in expressions if e] + if not expressions: + return '' + if len(expressions) == 1: + return expressions[0] + return 'and(' + ','.join(expressions) + ')' + +def contains(field: str, value: str) -> str: + return f"contains({field},'{value}')" + +def cmp(field: str, value: str, operator: str = 'eq') -> str: + op = operator.lower() + if op not in ('eq','ne','contains'): + raise ValueError("Unsupported operator: must be 'eq', 'ne' or 'contains") + return f"{op}({field},'{value}')" + +def cmpdefault(field: str, value: str, operator: str = 'eq') -> str: + op = 'eq' + return f"{op}({field},'{value}')" + +def in_list(field: str, values: List[str], operator: str = 'eq') -> str: + # build or(eq(field,val1),eq(field,val2),...) or or(ne(...)) + return join_or([cmp(field, v, operator) for v in values]) + + +# --- Build filter expression --- +def build_filter(args) -> str: + clauses = [] + operator = args.operator.lower() if args.operator else 'eq' + + # objectUri (use cmp) + if args.uri and args.uri.lower() != 'none': + clauses.append(cmp('objectUri', args.uri, operator)) + + # containerUri (use cmp) + if args.container and args.container.lower() != 'none': + clauses.append(cmp('containerUri', args.container, operator)) + + # description contains (CASE SENSITIVE as original) + if args.description and args.description != 'none': + clauses.append(contains('description', args.description)) + + # principal or principalType (use cmp for eq/ne) + if args.principal and args.principal.lower() != 'none': + p = args.principal + if p.lower() == 'authenticatedusers': + p = 'authenticatedUsers' + if p in PRINCIPAL_TYPES: + if operator in ['eq','ne']: + clauses.append(cmp('principalType', p, operator)) + else: + clauses.append(cmpdefault('principalType', p, operator)) + else: + clauses.append(cmp('principal', p, operator)) + + # enabled boolean (use cmp if operator is 'eq' or 'ne') + if args.enabled and args.enabled.lower() != 'none': + b = parse_bool(args.enabled) + if b is not None and operator in ['eq','ne']: + clauses.append(cmp('enabled', 'true' if b else 'false', operator)) + else: + clauses.append(cmpdefault('enabled', 'true' if b else 'false', operator)) + + ''' YET TO BE DEVELOPED + # permissions: ensure valid and build contains/any logic + if args.permission: + invalid = [p for p in args.permission if p not in VALID_PERMISSIONS] + if invalid: + raise SystemExit(f"Invalid permission(s): {invalid}. Valid: {sorted(VALID_PERMISSIONS)}") + # permissions are matched with contains; operator doesn't apply to contains + perm_clauses = [contains('permissions', p) for p in args.permission] + clauses.append(join_or(perm_clauses)) + ''' + + # created/modified ranges (these are range ops and unaffected by eq/ne) + if args.created_after: + clauses.append(f"gt(creationTimeStamp,'{args.created_after}')") + if args.created_before: + clauses.append(f"lt(creationTimeStamp,'{args.created_before}')") + if args.modified_after: + clauses.append(f"gt(modifiedTimeStamp,'{args.modified_after}')") + if args.modified_before: + clauses.append(f"lt(modifiedTimeStamp,'{args.modified_before}')") + + # created/modified by (use cmpdefault) + if args.created_by and args.created_by.lower() != 'none': + clauses.append(cmpdefault('createdBy', args.created_by, operator)) + if args.modified_by and args.modified_by.lower() != 'none': + clauses.append(cmpdefault('modifiedBy', args.modified_by, operator)) + + # mediaType exact match (use cmp) + if args.media_type and args.media_type.lower() != 'none': + clauses.append(cmp('mediaType', args.media_type, operator)) + + # condition free text (use contains) + if args.condition: + clauses.append(contains('condition', args.condition)) +# print(clauses) + + ''' YET TO BE DEVELOPED + if args.match_params: + clauses.append(contains('matchParams', args.match_params)) + ''' - if ident=='guest' or ident=='everyone' or ident=='authenticatedUsers': - reqval= "/authorization/rules?filter=and(eq(principalType,'"+ident+"'),contains(objectUri,'"+objuri+"'))" - else: - reqval= "/authorization/rules?filter=and(eq(principal,'"+ident+"'),contains(objectUri,'"+objuri+"'))" + # custom raw filter (allow advanced users to pass a full filter expression) + if args.raw_filter: + clauses.append(args.raw_filter) -if ident=='none' and objuri=='none': reqval=reqval+'?limit='+str(limitval) -else: reqval=reqval+'&limit='+str(limitval) + # combine all clauses with and + return join_and(clauses) -reqtype='get' -#make the rest call -rules_result_json=callrestapi(reqval,reqtype) +# --- Argument parsing --- +parser = argparse.ArgumentParser(description="listrules.py functions") -#print(rules_result_json) -#print('rules_result_json is a '+type(rules_result_json).__name__+' object') #rules_result_json is a dict object +parser.add_argument("-u","--uri", help="objectUri search, can be used with operator", default="none") +parser.add_argument("-c","--container", help="containerUri search, can be used with operator", default="none") +parser.add_argument("-p","--principal", help="Principal/Group ID, or 'authenticatedUsers', 'everyone' or 'guest'", default='none') +parser.add_argument("-d","--description", help="description search (contains only and CASE SENSITIVE)", default='none') +parser.add_argument("--condition", help="condition search (contains only and CASE SENSITIVE)") +parser.add_argument("--media-type", dest='media_type', help="mediaType search, can be used with operator") +parser.add_argument("-e","--enabled", help="Show rules enabled/true or disabled/false", default='none') +parser.add_argument("--operator", help="Filter operator", choices=['eq','ne','contains'], default='eq') +parser.add_argument("-o","--output", help="Output Style", choices=['csv','json','simple','simplejson'], default='json') +parser.add_argument("--headeroff", action='store_true', help="(Optional) Disables header when Output Style is csv") +#parser.add_argument("--perms", help="Sting of permissions to search for in the rules", default="none") +#parser.add_argument("--permission", action='append', help="Filter by permission. Can be repeated", metavar='PERM') +parser.add_argument("--created-after", dest='created_after', type=parse_date, help="Created after datetime, e.g. 2026-01-01T00:00:00Z") +parser.add_argument("--created-before", dest='created_before', type=parse_date, help="Created before datetime, e.g. 2026-01-01T00:00:00Z") +parser.add_argument("--modified-after", dest='modified_after', type=parse_date, help="Modified after datetime, e.g. 2026-01-01T00:00:00Z") +parser.add_argument("--modified-before", dest='modified_before', type=parse_date, help="Modified before datetime, e.g. 2026-01-01T00:00:00Z") +parser.add_argument("--created-by", dest="created_by", help="createdBy search (exact match only)") +parser.add_argument("--modified-by", dest="modified_by", help="modifiedBy search (exact match only)") +#parser.add_argument("--match-params", dest='match_params', help="Filter by matchParams contains text") +parser.add_argument("--printfilter", action='store_true', help="Returns the filter string without executing it") +parser.add_argument("--raw-filter", help="Raw filter expression to append (advanced users)") + +args = parser.parse_args() -#print the result if output style is json or simple -if output_style in ['json','simple']: - printresult(rules_result_json,output_style) -elif output_style=='csv': - # Print a header row - print(','.join(map(str,desired_output_columns))) +# --- Build request path --- +try: + filter_expr = build_filter(args) +except SystemExit as e: + print(str(e), file=sys.stderr) + sys.exit(2) +except ValueError as e: + print(str(e), file=sys.stderr) + sys.exit(2) + +base = "/authorization/rules" +if filter_expr: + reqval = f"{base}?filter={filter_expr}&limit={LIMITVAL}" +else: + reqval = f"{base}?limit={LIMITVAL}" + +reqtype = 'get' + +# prints the filter and REST call without executing, then exists +if args.printfilter: + print('\nFull filter:\n\033[1;32m'+reqval+'\033[0m\n') + print('Full call:\n\033[1;32m'+getbaseurl()+reqval+'\033[0m\n') + sys.exit(0) + +# make the rest call +rules_result_json = callrestapi(reqval, reqtype) + +# print the result if output style is json or simple +if args.output in ['json','simple']: + printresult(rules_result_json,args.output) +elif args.output=='csv': + + # option to disable header row + if args.headeroff: + pass + else: + print(','.join(map(str,DESIRED_OUTPUT_COLUMNS))) + if 'items' in rules_result_json: - #print "There are " + str(rules_result_json['count']) + " rules" for item in rules_result_json['items']: outstr='' - #print(item) - for column in desired_output_columns: + for column in DESIRED_OUTPUT_COLUMNS: # Add a comma to the output string, even if we will not output anything else, unless this is the very first desired output column - if column is not desired_output_columns[0]: outstr=outstr+',' + if column is not DESIRED_OUTPUT_COLUMNS[0]: outstr=outstr+',' if column=='setting': # The setting value is derived from two columns: type and condition. if 'condition' in item: @@ -108,7 +272,7 @@ outstr=outstr+'[' permstr='' # Output permissions in the order we choose, not the order they appear in the result item - for permission in valid_permissions: + for permission in VALID_PERMISSIONS: for result_permission in item['permissions']: if permission == result_permission: # Add a space to separate permissions if this isn't the first permission @@ -121,5 +285,4 @@ outstr=outstr+str(item[column]) print(outstr) else: - print ("output_style can be json, simple or csv. You specified " + output_style + " which is invalid.") - + print ("output_style can be json, simple or csv. You specified " + args.output + " which is invalid.") diff --git a/newlistrules.py b/newlistrules.py deleted file mode 100644 index 6d19d29..0000000 --- a/newlistrules.py +++ /dev/null @@ -1,273 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# listrules.py -# May 2026 -# -# Change History -# May 2026 - Initial release -# -# Copyright © 2026, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the License); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Assisted-by: Microsoft Copilot - -import argparse -import datetime -import json -import sys -from typing import List, Optional - -from sharedfunctions import callrestapi, printresult - - -# --- Configuration --- -LIMITVAL = 10000 -DESIRED_OUTPUT_COLUMNS = ['objectUri','containerUri','principalType','principal','setting','permissions','description','reason','createdBy','createdTimestamp','modifiedBy','modifiedTimestamp','condition','matchParams','mediaType','enabled','version','id'] -VALID_PERMISSIONS = {'read','update','delete','secure','add','remove','create'} -PRINCIPAL_TYPES = {'guest','everyone','authenticatedUsers'} - - -# --- Helpers --- -def parse_bool(val: str) -> Optional[bool]: - if val is None or val.lower() in ('none',''): - return None - v = val.lower() - if v in ('true','t','1','yes','y','enabled'): - return True - if v in ('false','f','0','no','n','disabled'): - return False - raise argparse.ArgumentTypeError(f"Invalid boolean value: {val}") - -def parse_date(val: str) -> str: - # Accept ISO 8601 date or datetime; raise on invalid - try: - # allow date-only or datetime - if 'T' in val: - datetime.datetime.fromisoformat(val.replace('Z','+00:00')) - else: - datetime.date.fromisoformat(val) - return val - except Exception: - raise argparse.ArgumentTypeError(f"Invalid date/datetime: {val}. Use ISO 8601 format.") - -def join_or(expressions: List[str]) -> str: - expressions = [e for e in expressions if e] - if not expressions: - return '' - if len(expressions) == 1: - return expressions[0] - return 'or(' + ','.join(expressions) + ')' - -def join_and(expressions: List[str]) -> str: - expressions = [e for e in expressions if e] - if not expressions: - return '' - if len(expressions) == 1: - return expressions[0] - return 'and(' + ','.join(expressions) + ')' - -def contains(field: str, value: str) -> str: - return f"contains({field},'{value}')" - -def cmp(field: str, value: str, operator: str = 'eq') -> str: - op = operator.lower() - if op not in ('eq','ne','contains'): - raise ValueError("Unsupported operator: must be 'eq', 'ne' or 'contains") - return f"{op}({field},'{value}')" - -def cmpdefault(field: str, value: str, operator: str = 'eq') -> str: - op = 'eq' - return f"{op}({field},'{value}')" - -def in_list(field: str, values: List[str], operator: str = 'eq') -> str: - # build or(eq(field,val1),eq(field,val2),...) or or(ne(...)) - return join_or([cmp(field, v, operator) for v in values]) - - -# --- Build filter expression --- -def build_filter(args) -> str: - clauses = [] - operator = args.operator.lower() if args.operator else 'eq' - - # objectUri (use cmp) - if args.uri and args.uri.lower() != 'none': - clauses.append(cmp('objectUri', args.uri, operator)) - - # containerUri cmp - if args.container and args.container.lower() != 'none': - clauses.append(cmp('containerUri', args.container, operator)) - - # description contains (CASE SENSITIVE as original) - if args.description and args.description != 'none': - clauses.append(contains('description', args.description)) - - # principal or principalType (use cmp for eq/ne) - if args.principal and args.principal.lower() != 'none': - p = args.principal - if p.lower() == 'authenticatedusers': - p = 'authenticatedUsers' - if p in PRINCIPAL_TYPES: - if operator in ['eq','ne']: - clauses.append(cmp('principalType', p, operator)) - else: - clauses.append(cmpdefault('principalType', p, operator)) - else: - clauses.append(cmp('principal', p, operator)) - - # enabled boolean (use cmp if operator is 'eq' or 'ne') - if args.enabled and args.enabled.lower() != 'none': - b = parse_bool(args.enabled) - if b is not None and operator in ['eq','ne']: - clauses.append(cmp('enabled', 'true' if b else 'false', operator)) - else: - clauses.append(cmpdefault('enabled', 'true' if b else 'false', operator)) - - ''' YET TO BE DEVELOPED - # permissions: ensure valid and build contains/any logic - if args.permission: - invalid = [p for p in args.permission if p not in VALID_PERMISSIONS] - if invalid: - raise SystemExit(f"Invalid permission(s): {invalid}. Valid: {sorted(VALID_PERMISSIONS)}") - # permissions are matched with contains; operator doesn't apply to contains - perm_clauses = [contains('permissions', p) for p in args.permission] - clauses.append(join_or(perm_clauses)) - ''' - - # created/modified ranges (these are range ops and unaffected by eq/ne) - if args.created_after: - clauses.append(f"gt(createdTimestamp,'{args.created_after}')") - if args.created_before: - clauses.append(f"lt(createdTimestamp,'{args.created_before}')") - if args.modified_after: - clauses.append(f"gt(modifiedTimestamp,'{args.modified_after}')") - if args.modified_before: - clauses.append(f"lt(modifiedTimestamp,'{args.modified_before}')") - - # mediaType exact match (use cmp) - if args.media_type and args.media_type.lower() != 'none': - clauses.append(cmp('mediaType', args.media_type, operator)) - - # condition free text (use contains) - if args.condition: - clauses.append(contains('condition', args.condition)) -# print(clauses) - - ''' YET TO BE DEVELOPED - if args.match_params: - clauses.append(contains('matchParams', args.match_params)) - ''' - - # custom raw filter (allow advanced users to pass a full filter expression) - if args.raw_filter: - clauses.append(args.raw_filter) - - # combine all clauses with and - return join_and(clauses) - - -# --- Argument parsing --- -parser = argparse.ArgumentParser(description="newlistrules.py functions") - -parser.add_argument("-u","--uri", help="objectUri search, can be used with operator", default="none") -parser.add_argument("-c","--container", help="containerUri search, can be used with operator", default="none") -parser.add_argument("-p","--principal", help="Principal/Group ID, or 'authenticatedUsers', 'everyone' or 'guest'", default='none') -parser.add_argument("-d","--description", help="description search (contains only and CASE SENSITIVE)", default='none') -parser.add_argument("--condition", help="condition search (contains only and CASE SENSITIVE)") -parser.add_argument("--media-type", dest='media_type', help="mediaType search, can be used with operator") -parser.add_argument("-e","--enabled", help="Show rules enabled/true or disabled/false", default='none') -parser.add_argument("--operator", help="Filter operator", choices=['eq','ne','contains'], default='eq') -parser.add_argument("-o","--output", help="Output Style", choices=['csv','json','simple','simplejson'], default='json') -parser.add_argument("--csvheader", help="(Optional) Disables header - must be combined with '-o csv'", choices=['yes','no'], default='yes') -#parser.add_argument("--perms", help="Sting of permissions to search for in the rules", default="none") -#parser.add_argument("--permission", action='append', help="Filter by permission. Can be repeated", metavar='PERM') -parser.add_argument("--created-after", dest='created_after', type=parse_date, help="Created after datetime, e.g. 2026-01-01T00:00:00Z") -parser.add_argument("--created-before", dest='created_before', type=parse_date, help="Created before datetime, e.g. 2026-01-01T00:00:00Z") -parser.add_argument("--modified-after", dest='modified_after', type=parse_date, help="Modified after datetime, e.g. 2026-01-01T00:00:00Z") -parser.add_argument("--modified-before", dest='modified_before', type=parse_date, help="Modified before datetime, e.g. 2026-01-01T00:00:00Z") -#parser.add_argument("--match-params", dest='match_params', help="Filter by matchParams contains text") -parser.add_argument("--raw-filter", help="Raw filter expression to append (advanced users)") - - -args = parser.parse_args() - -# --- Build request path --- -try: - filter_expr = build_filter(args) -except SystemExit as e: - print(str(e), file=sys.stderr) - sys.exit(2) -except ValueError as e: - print(str(e), file=sys.stderr) - sys.exit(2) - -base = "/authorization/rules" -if filter_expr: - reqval = f"{base}?filter={filter_expr}&limit={LIMITVAL}" -else: - reqval = f"{base}?limit={LIMITVAL}" - -reqtype = 'get' - -# make the rest call -rules_result_json = callrestapi(reqval, reqtype) - - -#print the result if output style is json or simple -if args.output in ['json','simple']: - printresult(rules_result_json,args.output) -elif args.output=='csv': - if args.csvheader=='yes': - # Print a header row - print(','.join(map(str,DESIRED_OUTPUT_COLUMNS))) - else: - pass - if 'items' in rules_result_json: - for item in rules_result_json['items']: - outstr='' - for column in DESIRED_OUTPUT_COLUMNS: - # Add a comma to the output string, even if we will not output anything else, unless this is the very first desired output column - if column is not DESIRED_OUTPUT_COLUMNS[0]: outstr=outstr+',' - if column=='setting': - # The setting value is derived from two columns: type and condition. - if 'condition' in item: - #print("Condition found") - outstr=outstr+'conditional '+item['type'] - else: - outstr=outstr+item['type'] - elif column in item: - # This column is in the results item for this rule - # Most columns are straight strings, but a few need special handling - if column in ['condition','description','reason']: - # The these strings can have values whcih contain commas, need we to quote them to avoid the commas being interpreted as column separators in the CSV - outstr=outstr+'"'+item[column]+'"' - elif column=='permissions': - # Construct a string listing each permission in the correct order, separated by spaces and surrounded by square brackets - outstr=outstr+'[' - permstr='' - # Output permissions in the order we choose, not the order they appear in the result item - for permission in VALID_PERMISSIONS: - for result_permission in item['permissions']: - if permission == result_permission: - # Add a space to separate permissions if this isn't the first permission - if not permstr=='': permstr=permstr+' ' - permstr=permstr+result_permission - outstr=outstr+permstr+']' - else: - # Normal column - # Some columns contain non-string values: matchParams and enabled are boolean, version is integer. Convert everything to a string. - outstr=outstr+str(item[column]) - print(outstr) -else: - print ("output_style can be json, simple or csv. You specified " + args.output + " which is invalid.") From d4180f934f9cd61f05bb4daea90be0a3525dc3f9 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 7 May 2026 13:35:44 +1200 Subject: [PATCH 5/6] new features and changes Changes: Added usage note to -h Added limit option Added sort-by option and error handling Added sort-order option Added count-only option Added error message for when no rules are found Changed: Changed 'printfilter' option to 'print-filter' Updated -h file Signed-off-by: Tom --- listrules.py | 83 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 71 insertions(+), 12 deletions(-) diff --git a/listrules.py b/listrules.py index 5e4fb61..59c6347 100755 --- a/listrules.py +++ b/listrules.py @@ -28,18 +28,28 @@ import json import sys from typing import List, Optional +from argparse import RawDescriptionHelpFormatter from sharedfunctions import callrestapi, printresult, getbaseurl # --- Configuration --- -LIMITVAL = 10000 DESIRED_OUTPUT_COLUMNS = ['objectUri','containerUri','principalType','principal','setting','permissions','description','reason','createdBy','creationTimeStamp','modifiedBy','modifiedTimeStamp','condition','matchParams','mediaType','enabled','version','id'] VALID_PERMISSIONS = ['read','update','delete','secure','add','remove','create'] PRINCIPAL_TYPES = {'guest','everyone','authenticatedUsers'} - # --- Helpers --- +# set results return limit +def set_limit(args, default: int = 10000) -> int: + val = getattr(args, "limit", None) + if val is None: + return default + try: + return int(val) + except (TypeError, ValueError): + raise ValueError(f"Invalid limit value: {val!r}; must be an integer") + + def parse_bool(val: str) -> Optional[bool]: if val is None or val.lower() in ('none',''): return None @@ -95,6 +105,18 @@ def in_list(field: str, values: List[str], operator: str = 'eq') -> str: # build or(eq(field,val1),eq(field,val2),...) or or(ne(...)) return join_or([cmp(field, v, operator) for v in values]) +def get_rules_count(rules_result_json): + """ + Return an integer count from the JSON. Falls back to len(items) if count missing or invalid. + """ + try: + count = rules_result_json.get("count", None) + if count is None: + return int(len(rules_result_json.get("items", []))) + return int(count) + except (TypeError, ValueError): + return int(len(rules_result_json.get("items", []))) + # --- Build filter expression --- def build_filter(args) -> str: @@ -184,28 +206,41 @@ def build_filter(args) -> str: # --- Argument parsing --- -parser = argparse.ArgumentParser(description="listrules.py functions") +parser = argparse.ArgumentParser( + description=''' +listrules.py\n +\033[4;34m\033[1mUSAGE NOTE\033[0m +All search filters use the \033[1;33m"eq"\033[0m filter unless otherwise stated. +When using the date/time based filters the format can be either: +\033[1;33m2026-01-01\033[0m +or +\033[1;33m2026-01-01T00:00:00Z\033[0m''', + formatter_class=RawDescriptionHelpFormatter) parser.add_argument("-u","--uri", help="objectUri search, can be used with operator", default="none") parser.add_argument("-c","--container", help="containerUri search, can be used with operator", default="none") -parser.add_argument("-p","--principal", help="Principal/Group ID, or 'authenticatedUsers', 'everyone' or 'guest'", default='none') -parser.add_argument("-d","--description", help="description search (contains only and CASE SENSITIVE)", default='none') -parser.add_argument("--condition", help="condition search (contains only and CASE SENSITIVE)") +parser.add_argument("-p","--principal", help="Principal/Group ID (CASE SENSITIVE), or 'authenticatedUsers', 'everyone' or 'guest'", default='none') +parser.add_argument("-d","--description", help="description search ('contains' only and CASE SENSITIVE)", default='none') +parser.add_argument("--condition", help="condition search ('contains' only and CASE SENSITIVE)") parser.add_argument("--media-type", dest='media_type', help="mediaType search, can be used with operator") parser.add_argument("-e","--enabled", help="Show rules enabled/true or disabled/false", default='none') parser.add_argument("--operator", help="Filter operator", choices=['eq','ne','contains'], default='eq') parser.add_argument("-o","--output", help="Output Style", choices=['csv','json','simple','simplejson'], default='json') -parser.add_argument("--headeroff", action='store_true', help="(Optional) Disables header when Output Style is csv") +parser.add_argument("--header-off", dest='headeroff', action='store_true', help="(Optional) Disables header when Output Style is csv") +parser.add_argument("--print-filter", dest='printfilter', action='store_true', help="Returns the filter string without executing it") #parser.add_argument("--perms", help="Sting of permissions to search for in the rules", default="none") #parser.add_argument("--permission", action='append', help="Filter by permission. Can be repeated", metavar='PERM') -parser.add_argument("--created-after", dest='created_after', type=parse_date, help="Created after datetime, e.g. 2026-01-01T00:00:00Z") -parser.add_argument("--created-before", dest='created_before', type=parse_date, help="Created before datetime, e.g. 2026-01-01T00:00:00Z") -parser.add_argument("--modified-after", dest='modified_after', type=parse_date, help="Modified after datetime, e.g. 2026-01-01T00:00:00Z") -parser.add_argument("--modified-before", dest='modified_before', type=parse_date, help="Modified before datetime, e.g. 2026-01-01T00:00:00Z") +parser.add_argument("--created-after", dest='created_after', type=parse_date, help="Created after datetime (see usage note above)") +parser.add_argument("--created-before", dest='created_before', type=parse_date, help="Created before datetime (see usage note above)") +parser.add_argument("--modified-after", dest='modified_after', type=parse_date, help="Modified after datetime (see usage note above)") +parser.add_argument("--modified-before", dest='modified_before', type=parse_date, help="Modified before datetime (see usage note above)") parser.add_argument("--created-by", dest="created_by", help="createdBy search (exact match only)") parser.add_argument("--modified-by", dest="modified_by", help="modifiedBy search (exact match only)") #parser.add_argument("--match-params", dest='match_params', help="Filter by matchParams contains text") -parser.add_argument("--printfilter", action='store_true', help="Returns the filter string without executing it") +parser.add_argument("--count-only", dest='countonly', action='store_true', help="Displays the number of rules found only") +parser.add_argument("--limit", type=int, help="(Optional) Overrides the default 10000 return limit with a custom value") +parser.add_argument("--sort-by", help="(Optional) Sorts results (single sortBy item ONLY)") +parser.add_argument("--sort-order", help="(Optional) Order of sorted results", choices=['ascending','descending'], default='ascending') parser.add_argument("--raw-filter", help="Raw filter expression to append (advanced users)") args = parser.parse_args() @@ -220,12 +255,25 @@ def build_filter(args) -> str: print(str(e), file=sys.stderr) sys.exit(2) + +LIMITVAL = set_limit(args) base = "/authorization/rules" if filter_expr: reqval = f"{base}?filter={filter_expr}&limit={LIMITVAL}" else: reqval = f"{base}?limit={LIMITVAL}" +if args.sort_by == None: + pass +elif args.sort_by != None and args.sort_by in DESIRED_OUTPUT_COLUMNS: + reqval = reqval + f"&sortBy=" + args.sort_by + ":" + args.sort_order +else: + print('\n\033[1;31mINVALID SORT OPTION\n\n'+ + '\033[1;33mUpdate your sort to use one of the following options and try again.\033[0m\n\n'+ + str(DESIRED_OUTPUT_COLUMNS) +'\n') + sys.exit(0) + + reqtype = 'get' # prints the filter and REST call without executing, then exists @@ -236,6 +284,17 @@ def build_filter(args) -> str: # make the rest call rules_result_json = callrestapi(reqval, reqtype) +rulesfound = get_rules_count(rules_result_json) +#print(rulesfound) + +if rulesfound == 0: + print('\n\033[1;31mNO RULES FOUND. Check your filter and try again.\n\n\033[1;33m'+reqval+'\033[0m\n') + sys.exit(0) +elif args.countonly: + print('\n\033[1;32m'+str(rulesfound)+' RULES FOUND\033[0m\n') + sys.exit(0) +else: + pass # print the result if output style is json or simple if args.output in ['json','simple']: From 6995cbd492779d5e6032a6267879fcefba78de83 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 8 May 2026 11:10:11 +1200 Subject: [PATCH 6/6] dco commit I, Tom , hereby add my Signed-off-by to this commit: 509b2a35d2f9e85d71452f31812f5d4630bc8cd3 I, Tom , hereby add my Signed-off-by to this commit: 7ebce128069f4168c0781bb5c093d227a38bcb36 I, Tom , hereby add my Signed-off-by to this commit: 3f6bcb60c9c6f6d30d98e761ad56b254efbb0415 I, Tom , hereby add my Signed-off-by to this commit: 5029cb83603eb721b07c828606b312410dbacc48 Signed-off-by: Tom