Repository: microsoft/sarif-tools (language: python)

Issue:
How to remove specific entries
I tried to `copy` while applying `blame-filter`, but it didn't work as expected. 

I want to ask if it is possible to provide a specific entry with `filename` and `lines` to be filtered out.


Discussion / hints:
Hi, sorry for the delay spotting the issue that you have raised.  Thank you for your interest in sarif-tools!

I checked the `copy` operation with blame-filter and it worked as I expect, i.e. it filters issues by author information as described in the [README](https://github.com/microsoft/sarif-tools/blob/e803a8863fd281b4886caa384c5f4efe1d291a06/README.md#blame-filtering).  The tool doesn't have a facility for filtering issues by filename or line location.  Is that a feature that you want - can you elaborate on your needs?

Fix patch:
diff --git a/.gitignore b/.gitignore
index 847388b..8ce87d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,13 @@
 *.pyc
 *.orig
 /.venv
-/.vscode
\ No newline at end of file
+/.vscode
+/.idea
+/.pytest_cache
+.DS_Store
+*.sarif
+*.json
+*.csv
+.coverage
+coverage.xml
+*filter.yaml
diff --git a/README.md b/README.md
index 4a6acb0..e677d70 100644
--- a/README.md
+++ b/README.md
@@ -150,12 +150,36 @@ sarif blame -o "C:\temp\sarif_files_with_blame_info" -c "C:\code\my_source_repo"
 
 If the current working directory is the git repository, the `-c` argument can be omitted.
 
-See [Blame filtering](#blame-filtering) below for the format of the blame information that gets added to the SARIF files.
+Blame information is added to the property bag of each `result` object for which it was successfully obtained.  The keys and values used are as in the [git blame porcelain format](https://git-scm.com/docs/git-blame#_the_porcelain_format).  E.g.:
+
+```json
+{
+  "ruleId": "SM00702",
+  ...
+  "properties": {
+    "blame": {
+      "author": "aperson",
+      "author-mail": "<aperson@acompany.com>",
+      "author-time": "1350899798",
+      "author-tz": "+0000",
+      "committer": "aperson",
+      "committer-mail": "<aperson@acompany.com>",
+      "committer-time": "1350899798",
+      "committer-tz": "+0000",
+      "summary": "blah blah commit comment blah",
+      "boundary": true,
+      "filename": "src/net/myproject/mypackage/MyClass.java"
+    }
+  }
+}
+```
+
+Note that the bare `boundary` key is given the automatic value `true`.
 
 #### codeclimate
 
 ```plain
-usage: sarif codeclimate [-h] [--output PATH] [--blame-filter FILE] [--autotrim] [--trim PREFIX] [file_or_dir ...]
+usage: sarif codeclimate [-h] [--output PATH] [--filter FILE] [--autotrim] [--trim PREFIX] [file_or_dir ...]
 
 Write a JSON representation in Code Climate format of SARIF file(s) for viewing as a Code Quality report in GitLab UI
 
@@ -166,8 +190,8 @@ optional arguments:
   -h, --help            show this help message and exit
   --output PATH, -o PATH
                         Output file or directory
-  --blame-filter FILE, -b FILE
-                        Specify the blame filter file to apply. See README for format.
+  --filter FILE, -b FILE
+                        Specify the filter file to apply. See README for format.
   --autotrim, -a        Strip off the common prefix of paths in the CSV output
   --trim PREFIX         Prefix to strip from issue paths, e.g. the checkout directory on the build agent
 ```
@@ -176,12 +200,12 @@ Write out a JSON file of Code Climate tool format from [a set of] SARIF files.
 This can then be published as a Code Quality report artefact in a GitLab pipeline and shown in GitLab UI for merge requests.
 
 The JSON output can also be filtered using the blame information; see
-[Blame filtering](#blame-filtering) below for how to use the `--blame-filter` option.
+[Filtering](#filtering) below for how to use the `--filter` option.
 
 #### copy
 
 ```plain
-usage: sarif copy [-h] [--output FILE] [--blame-filter FILE] [--timestamp] [file_or_dir [file_or_dir ...]]
+usage: sarif copy [-h] [--output FILE] [--filter FILE] [--timestamp] [file_or_dir [file_or_dir ...]]
 
 Write a new SARIF file containing optionally-filtered data from other SARIF file(s)
 
@@ -192,14 +216,14 @@ optional arguments:
   -h, --help            show this help message and exit
   --output FILE, -o FILE
                         Output file
-  --blame-filter FILE, -b FILE
-                        Specify the blame filter file to apply. See README for format.
+  --filter FILE, -b FILE
+                        Specify the filter file to apply. See README for format.
   --timestamp, -t       Append current timestamp to output filename in the "yyyymmddThhmmssZ" format used by the `sarif trend` command
 ```
 
 Write a new SARIF file containing optionally-filtered data from an existing SARIF file or multiple
 SARIF files.  The resulting file contains each run from the original SARIF files back-to-back.
-The results can be filtered (see [Blame filtering](#blame-filtering) below), in which case only
+The results can be filtered (see [Filtering](#filtering) below), in which case only
 those results from the original SARIF files that meet the filter are included; the output file
 contains no information about the excluded records.  If a run in the original file was empty,
 or all its results are filtered out, the empty run is still included.
@@ -217,7 +241,7 @@ a build process into a single file that can be more easily stored and processed
 #### csv
 
 ```plain
-usage: sarif csv [-h] [--output PATH] [--blame-filter FILE] [--autotrim] [--trim PREFIX] [file_or_dir [file_or_dir ...]]
+usage: sarif csv [-h] [--output PATH] [--filter FILE] [--autotrim] [--trim PREFIX] [file_or_dir [file_or_dir ...]]
 
 Write a CSV file listing the issues from the SARIF files(s) specified
 
@@ -228,8 +252,8 @@ optional arguments:
   -h, --help            show this help message and exit
   --output PATH, -o PATH
                         Output file or directory
-  --blame-filter FILE, -b FILE
-                        Specify the blame filter file to apply. See README for format.
+  --filter FILE, -b FILE
+                        Specify the filter file to apply. See README for format.
   --autotrim, -a        Strip off the common prefix of paths in the CSV output
   --trim PREFIX         Prefix to strip from issue paths, e.g. the checkout directory on the build agent
 ```
@@ -254,12 +278,12 @@ If the SARIF file(s) contain blame information (as added by the `blame` command)
 includes an "Author" column indicating who last modified the line in question.
 
 The CSV output can also be filtered using the same blame information; see
-[Blame filtering](#blame-filtering) below for how to use the `--blame-filter` option.
+[Filtering](#filtering) below for how to use the `--filter` option.
 
 #### diff
 
 ```plain
-usage: sarif diff [-h] [--output FILE] [--blame-filter FILE] old_file_or_dir new_file_or_dir
+usage: sarif diff [-h] [--output FILE] [--filter FILE] old_file_or_dir new_file_or_dir
 
 Find the difference between two [sets of] SARIF files
 
@@ -271,8 +295,8 @@ optional arguments:
   -h, --help            show this help message and exit
   --output FILE, -o FILE
                         Output file
-  --blame-filter FILE, -b FILE
-                        Specify the blame filter file to apply. See README for format.
+  --filter FILE, -b FILE
+                        Specify the filter file to apply. See README for format.
 ```
 
 Print the difference between two [sets of] SARIF files.
@@ -358,12 +382,12 @@ output JSON file, all new locations are written, but when writing output to the
 of three locations are shown.  Note that there can be some false positives here, if line numbers
 have changed.
 
-See [Blame filtering](#blame-filtering) below for how to use the `--blame-filter` option.
+See [Filtering](#filtering) below for how to use the `--filter` option.
 
 #### emacs
 
 ```plain
-usage: sarif emacs [-h] [--output PATH] [--blame-filter FILE] [--no-autotrim] [--image IMAGE] [--trim PREFIX] [file_or_dir [file_or_dir ...]]
+usage: sarif emacs [-h] [--output PATH] [--filter FILE] [--no-autotrim] [--image IMAGE] [--trim PREFIX] [file_or_dir [file_or_dir ...]]
 
 Write a representation of SARIF file(s) for viewing in emacs
 
@@ -374,8 +398,8 @@ optional arguments:
   -h, --help            show this help message and exit
   --output PATH, -o PATH
                         Output file or directory
-  --blame-filter FILE, -b FILE
-                        Specify the blame filter file to apply. See README for format.
+  --filter FILE, -b FILE
+                        Specify the filter file to apply. See README for format.
   --no-autotrim, -n     Do not strip off the common prefix of paths in the output document
   --image IMAGE         Image to include at top of file - SARIF logo by default
   --trim PREFIX         Prefix to strip from issue paths, e.g. the checkout directory on the build agent
@@ -384,7 +408,7 @@ optional arguments:
 #### html
 
 ```plain
-usage: sarif html [-h] [--output PATH] [--blame-filter FILE] [--no-autotrim] [--image IMAGE] [--trim PREFIX] [file_or_dir [file_or_dir ...]]
+usage: sarif html [-h] [--output PATH] [--filter FILE] [--no-autotrim] [--image IMAGE] [--trim PREFIX] [file_or_dir [file_or_dir ...]]
 
 Write an HTML representation of SARIF file(s) for viewing in a web browser
 
@@ -395,8 +419,8 @@ optional arguments:
   -h, --help            show this help message and exit
   --output PATH, -o PATH
                         Output file or directory
-  --blame-filter FILE, -b FILE
-                        Specify the blame filter file to apply. See README for format.
+  --filter FILE, -b FILE
+                        Specify the filter file to apply. See README for format.
   --no-autotrim, -n     Do not strip off the common prefix of paths in the output document
   --image IMAGE         Image to include at top of file - SARIF logo by default
   --trim PREFIX         Prefix to strip from issue paths, e.g. the checkout directory on the build agent
@@ -412,7 +436,7 @@ Use the `--trim` option to strip specific prefixes from the paths, to make the g
 
 Use the `--image` option to provide a header image for the top of the HTML page.  The image is embedded into the HTML, so the HTML document remains a portable standalone file.
 
-See [Blame filtering](#blame-filtering) below for how to use the `--blame-filter` option.
+See [Filtering](#filtering) below for how to use the `--filter` option.
 
 #### info
 
@@ -470,7 +494,7 @@ sarif ls "C:\temp\sarif_files" "C:\temp\sarif_with_date"
 #### summary
 
 ```plain
-usage: sarif summary [-h] [--output PATH] [--blame-filter FILE] [file_or_dir [file_or_dir ...]]
+usage: sarif summary [-h] [--output PATH] [--filter FILE] [file_or_dir [file_or_dir ...]]
 
 Write a text summary with the counts of issues from the SARIF files(s) specified
 
@@ -481,8 +505,8 @@ optional arguments:
   -h, --help            show this help message and exit
   --output PATH, -o PATH
                         Output file or directory
-  --blame-filter FILE, -b FILE
-                        Specify the blame filter file to apply. See README for format.
+  --filter FILE, -b FILE
+                        Specify the filter file to apply. See README for format.
 ```
 
 Print a summary of the issues in one or more SARIF file(s), grouped by severity and then ordered by number of occurrences.
@@ -499,12 +523,12 @@ When no output directory or file is specified, the overall summary is printed to
 sarif summary "C:\temp\sarif_files\devskim_myapp.sarif"
 ```
 
-See [Blame filtering](#blame-filtering) below for how to use the `--blame-filter` option.
+See [Filtering](#filtering) below for how to use the `--filter` option.
 
 #### trend
 
 ```plain
-usage: sarif trend [-h] [--output FILE] [--blame-filter FILE] [--dateformat {dmy,mdy,ymd}] [file_or_dir [file_or_dir ...]]
+usage: sarif trend [-h] [--output FILE] [--filter FILE] [--dateformat {dmy,mdy,ymd}] [file_or_dir [file_or_dir ...]]
 
 Write a CSV file with time series data from SARIF files with "yyyymmddThhmmssZ" timestamps in their filenames
 
@@ -515,8 +539,8 @@ optional arguments:
   -h, --help            show this help message and exit
   --output FILE, -o FILE
                         Output file
-  --blame-filter FILE, -b FILE
-                        Specify the blame filter file to apply. See README for format.
+  --filter FILE, -b FILE
+                        Specify the filter file to apply. See README for format.
   --dateformat {dmy,mdy,ymd}, -f {dmy,mdy,ymd}
                         Date component order to use in output CSV. Default is `dmy`
 ```
@@ -530,7 +554,7 @@ The CSV can be loaded in Microsoft Excel for graphing and trend analysis.
 sarif trend -o timeline.csv "C:\temp\sarif_with_date" --dateformat dmy
 ```
 
-See [Blame filtering](#blame-filtering) below for how to use the `--blame-filter` option.
+See [Filtering](#filtering) below for how to use the `--filter` option.
 
 #### usage
 
@@ -550,7 +574,7 @@ Print usage and exit.
 #### word
 
 ```plain
-usage: sarif word [-h] [--output PATH] [--blame-filter FILE] [--no-autotrim] [--image IMAGE] [--trim PREFIX] [file_or_dir [file_or_dir ...]]
+usage: sarif word [-h] [--output PATH] [--filter FILE] [--no-autotrim] [--image IMAGE] [--trim PREFIX] [file_or_dir [file_or_dir ...]]
 
 Produce MS Word .docx summaries of the SARIF files specified
 
@@ -561,8 +585,8 @@ optional arguments:
   -h, --help            show this help message and exit
   --output PATH, -o PATH
                         Output file or directory
-  --blame-filter FILE, -b FILE
-                        Specify the blame filter file to apply. See README for format.
+  --filter FILE, -b FILE
+                        Specify the filter file to apply. See README for format.
   --no-autotrim, -n     Do not strip off the common prefix of paths in the output document
   --image IMAGE         Image to include at top of file - SARIF logo by default
   --trim PREFIX         Prefix to strip from issue paths, e.g. the checkout directory on the build agent
@@ -589,81 +613,86 @@ Use the `--trim` option to strip specific prefixes from the paths, to make the g
 
 Use the `--image` option to provide a header image for the top of the Word document.
 
-See [Blame filtering](#blame-filtering) below for how to use the `--blame-filter` option.
-
-## Blame filtering
-
-Use the `sarif blame` command to augment a SARIF file or multiple SARIF files with blame information.
-
-Blame information is added to the property bag of each `result` object for which it was successfully obtained.  The keys and values used are as in the [git blame porcelain format](https://git-scm.com/docs/git-blame#_the_porcelain_format).  E.g.:
-
-```json
-{
-  "ruleId": "SM00702",
-  ...
-  "properties": {
-    "blame": {
-      "author": "aperson",
-      "author-mail": "<aperson@acompany.com>",
-      "author-time": "1350899798",
-      "author-tz": "+0000",
-      "committer": "aperson",
-      "committer-mail": "<aperson@acompany.com>",
-      "committer-time": "1350899798",
-      "committer-tz": "+0000",
-      "summary": "blah blah commit comment blah",
-      "boundary": true,
-      "filename": "src/net/myproject/mypackage/MyClass.java"
-    }
-  }
-}
-```
+See [Filtering](#filtering) below for how to use the `--filter` option.
 
-Note that the bare `boundary` key is given the automatic value `true`.
+## Filtering
 
-This blame data can then be used for filtering and summarising via the `--blame-filter` option available for various commands.  This option requires a path to a filter-list file, containing a list of patterns and substrings to match against the blame information author email.  The format of a filter-list file is as follows:
+The data in each `result` object can then be used for filtering via the `--filter` option available for various commands.  This option requires a path to a filter-list YAML file, containing a list of patterns and substrings to match against data in a SARIF file.  The format of a filter-list file is as follows:
 
-```plain
+```yaml
 # Lines beginning with # are interpreted as comments and ignored.
-# A line beginning with "description: " is interpreted as an optional description for the filter.  If no title is specified, the filter file name is used.
+# Optional description for the filter.  If no title is specified, the filter file name is used.
 description: Example filter from README.md
-# Lines beginning with "+: " are interpreted as inclusion substrings.  E.g. the following line includes issues whose author-mail field contains "@microsoft.com".
-+: @microsoft.com
-# The "+: " can be omitted.
-@microsoft.com
-# Instead of a substring, a regular expression can be used, enclosed in "/" characters.  Issues whose author-mail field includes a string matching the regular expression are included.  Use ^ and $ to match the whole author-mail field.
-+: /^<myname.*\.com>$/
-# Again, the "+: " can be omitted for a regular expression include pattern.
-/^<myname.*\.com>$/
-# Lines beginning with "-: " are interpreted as exclusion substrings.  E.g. the following line excludes issues whose author-mail field contains "bot@microsoft.com".
--: bot@microsoft.com
-# Instead of a substring, a regular expression can be used, enclosed in "/" characters.  Issues whose author-mail field includes a string matching the regular expression are excluded.  Use ^ and $ to match the whole author-mail field.  E.g. the following pattern excludes all issues whose author-mail field contains a GUID.
--: /[0-9A-F]{8}[-][0-9A-F]{4}[-][0-9A-F]{4}[-][0-9A-F]{4}[-][0-9A-F]{12}/
+
+# Optional configuration section to override default values.
+configuration:
+  # This option controls whether to include results where a property to check is missing, default value is true.
+  default-include: false
+
+# Items in `include` list are interpreted as inclusion filtering rules. 
+# Items are treated with OR operator, the filtered results includes objects matching any rule.
+# Each item can be one rule or a list of rules, in the latter case rules in the list are treated with AND operator - all rules must match.
+include:
+  # The following line includes issues whose author-mail property contains "@microsoft.com" AND found in Java files. 
+  # Values with special characters `\:;_()$%^@,` must be enclosed in quotes (single or double):
+  - author-mail: "@microsoft.com"
+    locations[*].physicalLocation.artifactLocation.uri: "*.java"
+  # Instead of a substring, a regular expression can be used, enclosed in "/" characters.  
+  # Issues whose committer-mail property includes a string matching the regular expression are included.  
+  # Use ^ and $ to match the whole committer-mail property.
+  - committer-mail: 
+      value: "/^<myname.*\\.com>$/"
+      # Configuration options can be overriden for any rule.
+      default-include: true
+
+# Lines under `exclude` are interpreted as exclusion filtering rules.
+exclude:
+  # The following line excludes issues whose location is in test Java files with names starting with the "Test" prefix.
+  - location: "Test*.java"
+  # The value for the property can be empty, in this case only existence of the property is checked. 
+  - suppression:
 ```
 
 Here's an example of a filter-file that includes issues on lines changed by an `@microsoft.com` email address or a `myname.SOMETHING.com` email address, but not if those email addresses end in `bot@microsoft.com` or contain a GUID.  It's the same as the above example, with comments stripped out.
 
-```plain
+```yaml
 description: Example filter from README.md
-+: @microsoft.com
-+: /^<myname.*\.com>$/
--: bot@microsoft.com
--: /[0-9A-F]{8}[-][0-9A-F]{4}[-][0-9A-F]{4}[-][0-9A-F]{4}[-][0-9A-F]{12}/
-```
-
-All matching is case insensitive, because email addresses are.  Whitespace at the start and end of lines is ignored, which also means that line ending characters don't matter.  The blame filter file must be UTF-8 encoded (including plain ASCII7).  It can have a byte order mark or not.
+include:
+  - author-mail: "@microsoft.com"
+  - author-mail: "/myname\\..*\\.com/"
+exclude:
+  - author-mail: bot@microsoft.com
+  - author-mail: '/[0-9A-F]{8}[-][0-9A-F]{4}[-][0-9A-F]{4}[-][0-9A-F]{4}[-][0-9A-F]{12}\@microsoft.com/'
+```
+
+Field names must be specified in [JSONPath notation](https://goessner.net/articles/JsonPath/)
+accessing data in the [SARIF `result` object](https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Toc16012594).
+
+For commonly used properties the following shortcuts are defined:
+| Shortcut | Full JSONPath |
+| -------- | -------- |
+| author | properties.blame.author |
+| author-mail | properties.blame.author-mail |
+| committer | properties.blame.committer |
+| committer-mail | properties.blame.committer-mail |
+| location | locations[*].physicalLocation.artifactLocation.uri |
+| rule | ruleId |
+| suppression | suppressions[*].kind |
+
+For the property `uri` (e.g. in `locations[*].physicalLocation.artifactLocation.uri`) file name wildcard characters can be used as it represents a file location:
+- `?` - a single occurrence of any character in a directory or file name
+- `*` - zero or more occurrences of any character in a directory or file name
+- `**` - zero or more occurrences across multiple directory levels
+
+E.g.
+- `tests/Test???.js`
+- `src/js/*.js`
+- `src/js/**/*.js`
+
+All matching is case insensitive, because email addresses are.  Whitespace at the start and end of lines is ignored, which also means that line ending characters don't matter.  The filter file must be UTF-8 encoded (including plain ASCII7).
 
 If there are no inclusion patterns, all issues are included except for those matching the exclusion patterns.  If there are inclusion patterns, only issues matching the inclusion patterns are included.  If an issue matches one or more inclusion patterns and also at least one exclusion pattern, it is excluded.
 
-Sometimes, there may be issues in the SARIF file to which the filter cannot be applied, because blame information is not available.  This can be for two reasons: either there is no blame information recorded for the file in which the issue occurred, or the issue location lacks a line number (or specifies line number 1 as a placeholder) so that blame information cannot be correlated to the issue.  These issues are included by default.  To identify which issues these are, create a filter file that excludes everything to which the filter can be applied:
-
-```plain
-description: Exclude everything filterable
--: /.*/
-```
-
-Then run a `sarif` command using this filter file as the `--blame-filter` to see the default-included issues.
-
 ## Usage as a Python library
 
 Although not its primary purpose, you can use sarif-tools from a Python script or module to
@@ -753,11 +782,11 @@ These fields and methods allow access to the underlying information about the SA
 
 Call `init_path_prefix_stripping(autotrim, path_prefixes)` on a `SarifFileSet`, `SarifFile` or `SarifRun` object to set up path filtering, either automatically removing the longest common prefix (`autotrim=True`) or removing specific prefixes (`autotrim=False` and a list of strings in `path_prefixes`).
 
-#### Blame filtering API
+#### Filtering API
 
-Call `init_blame_filter(filter_description, include_substrings, include_regexes, exclude_substrings, exclude_regexes)` on a `SarifFileSet`, `SarifFile` or `SarifRun` object to set up blame filtering.  `filter_description` is a string and the other parameters are lists of strings (with no `/` characters around the regular expressions).  They correspond in an obvious way to the filter file contents described in [Blame filtering](#blame-filtering) above.
+Call `init_general_filter(filter_description, include_filters, exclude_filters)` on a `SarifFileSet`, `SarifFile` or `SarifRun` object to set up filtering.  `filter_description` is a string and the other parameters are lists of inclusion and exclusion rules.  They correspond in an obvious way to the filter file contents described in [Filtering](#filtering) above.
 
-Call `get_filter_stats()` to retrieve the filter stats after reading the results or records from sarif files.  It returns `None` if there is no filter, or otherwise a `sarif_file.FilterStats` object with integer fields `filtered_in_result_count`, `filtered_out_result_count`, `missing_blame_count` and `unconvincing_line_number_count`.  Call `to_string()` on the `FilterStats` object for a readable representation of these statistics, which also includes the filter file name or description (`filter_description` field).
+Call `get_filter_stats()` to retrieve the filter stats after reading the results or records from sarif files.  It returns `None` if there is no filter, or otherwise a `sarif_file.FilterStats` object with integer fields `filtered_in_result_count`, `filtered_out_result_count`.  Call `to_string()` on the `FilterStats` object for a readable representation of these statistics, which also includes the filter file name or description (`filter_description` field).
 
 ## Suggested usage in CI pipelines
 
@@ -797,7 +826,7 @@ sarif copy --timestamp -o artifacts/myapp_alltools_with_blame.sarif
 ```
 
 Download the file `myapp_alltools_with_blame_TIMESTAMP.sarif` that is generated.  Then later you can
-filter the results using the `--blame-filter` argument, or generate graph of code quality over time
+filter the results using the `--filter` argument, or generate graph of code quality over time
 using `sarif trend`.
 
 ## Credits
diff --git a/poetry.lock b/poetry.lock
index 6d2fb6b..1632345 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -121,6 +121,7 @@ files = [
     {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37"},
     {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa"},
     {file = "contourpy-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa"},
+    {file = "contourpy-1.1.0-cp310-cp310-win32.whl", hash = "sha256:9b2dd2ca3ac561aceef4c7c13ba654aaa404cf885b187427760d7f7d4c57cff8"},
     {file = "contourpy-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2"},
     {file = "contourpy-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882"},
     {file = "contourpy-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545"},
@@ -129,6 +130,7 @@ files = [
     {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e"},
     {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a"},
     {file = "contourpy-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e"},
+    {file = "contourpy-1.1.0-cp311-cp311-win32.whl", hash = "sha256:edb989d31065b1acef3828a3688f88b2abb799a7db891c9e282df5ec7e46221b"},
     {file = "contourpy-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256"},
     {file = "contourpy-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed"},
     {file = "contourpy-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae"},
@@ -137,6 +139,7 @@ files = [
     {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94"},
     {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f"},
     {file = "contourpy-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1"},
+    {file = "contourpy-1.1.0-cp38-cp38-win32.whl", hash = "sha256:108dfb5b3e731046a96c60bdc46a1a0ebee0760418951abecbe0fc07b5b93b27"},
     {file = "contourpy-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4"},
     {file = "contourpy-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9"},
     {file = "contourpy-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a"},
@@ -145,6 +148,7 @@ files = [
     {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f"},
     {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a"},
     {file = "contourpy-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002"},
+    {file = "contourpy-1.1.0-cp39-cp39-win32.whl", hash = "sha256:71551f9520f008b2950bef5f16b0e3587506ef4f23c734b71ffb7b89f8721999"},
     {file = "contourpy-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d"},
     {file = "contourpy-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493"},
     {file = "contourpy-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439"},
@@ -165,6 +169,73 @@ mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.2.0)", "types-Pill
 test = ["Pillow", "contourpy[test-no-images]", "matplotlib"]
 test-no-images = ["pytest", "pytest-cov", "wurlitzer"]
 
+[[package]]
+name = "coverage"
+version = "7.3.1"
+description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "coverage-7.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd0f7429ecfd1ff597389907045ff209c8fdb5b013d38cfa7c60728cb484b6e3"},
+    {file = "coverage-7.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:966f10df9b2b2115da87f50f6a248e313c72a668248be1b9060ce935c871f276"},
+    {file = "coverage-7.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0575c37e207bb9b98b6cf72fdaaa18ac909fb3d153083400c2d48e2e6d28bd8e"},
+    {file = "coverage-7.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:245c5a99254e83875c7fed8b8b2536f040997a9b76ac4c1da5bff398c06e860f"},
+    {file = "coverage-7.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c96dd7798d83b960afc6c1feb9e5af537fc4908852ef025600374ff1a017392"},
+    {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:de30c1aa80f30af0f6b2058a91505ea6e36d6535d437520067f525f7df123887"},
+    {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:50dd1e2dd13dbbd856ffef69196781edff26c800a74f070d3b3e3389cab2600d"},
+    {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9c0c19f70d30219113b18fe07e372b244fb2a773d4afde29d5a2f7930765136"},
+    {file = "coverage-7.3.1-cp310-cp310-win32.whl", hash = "sha256:770f143980cc16eb601ccfd571846e89a5fe4c03b4193f2e485268f224ab602f"},
+    {file = "coverage-7.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:cdd088c00c39a27cfa5329349cc763a48761fdc785879220d54eb785c8a38520"},
+    {file = "coverage-7.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:74bb470399dc1989b535cb41f5ca7ab2af561e40def22d7e188e0a445e7639e3"},
+    {file = "coverage-7.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:025ded371f1ca280c035d91b43252adbb04d2aea4c7105252d3cbc227f03b375"},
+    {file = "coverage-7.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6191b3a6ad3e09b6cfd75b45c6aeeffe7e3b0ad46b268345d159b8df8d835f9"},
+    {file = "coverage-7.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb0b188f30e41ddd659a529e385470aa6782f3b412f860ce22b2491c89b8593"},
+    {file = "coverage-7.3.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c8f0df9dfd8ff745bccff75867d63ef336e57cc22b2908ee725cc552689ec8"},
+    {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7eb3cd48d54b9bd0e73026dedce44773214064be93611deab0b6a43158c3d5a0"},
+    {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ac3c5b7e75acac31e490b7851595212ed951889918d398b7afa12736c85e13ce"},
+    {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b4ee7080878077af0afa7238df1b967f00dc10763f6e1b66f5cced4abebb0a3"},
+    {file = "coverage-7.3.1-cp311-cp311-win32.whl", hash = "sha256:229c0dd2ccf956bf5aeede7e3131ca48b65beacde2029f0361b54bf93d36f45a"},
+    {file = "coverage-7.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c6f55d38818ca9596dc9019eae19a47410d5322408140d9a0076001a3dcb938c"},
+    {file = "coverage-7.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5289490dd1c3bb86de4730a92261ae66ea8d44b79ed3cc26464f4c2cde581fbc"},
+    {file = "coverage-7.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ca833941ec701fda15414be400c3259479bfde7ae6d806b69e63b3dc423b1832"},
+    {file = "coverage-7.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd694e19c031733e446c8024dedd12a00cda87e1c10bd7b8539a87963685e969"},
+    {file = "coverage-7.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aab8e9464c00da5cb9c536150b7fbcd8850d376d1151741dd0d16dfe1ba4fd26"},
+    {file = "coverage-7.3.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87d38444efffd5b056fcc026c1e8d862191881143c3aa80bb11fcf9dca9ae204"},
+    {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8a07b692129b8a14ad7a37941a3029c291254feb7a4237f245cfae2de78de037"},
+    {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2829c65c8faaf55b868ed7af3c7477b76b1c6ebeee99a28f59a2cb5907a45760"},
+    {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f111a7d85658ea52ffad7084088277135ec5f368457275fc57f11cebb15607f"},
+    {file = "coverage-7.3.1-cp312-cp312-win32.whl", hash = "sha256:c397c70cd20f6df7d2a52283857af622d5f23300c4ca8e5bd8c7a543825baa5a"},
+    {file = "coverage-7.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:5ae4c6da8b3d123500f9525b50bf0168023313963e0e2e814badf9000dd6ef92"},
+    {file = "coverage-7.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ca70466ca3a17460e8fc9cea7123c8cbef5ada4be3140a1ef8f7b63f2f37108f"},
+    {file = "coverage-7.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f2781fd3cabc28278dc982a352f50c81c09a1a500cc2086dc4249853ea96b981"},
+    {file = "coverage-7.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6407424621f40205bbe6325686417e5e552f6b2dba3535dd1f90afc88a61d465"},
+    {file = "coverage-7.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:04312b036580ec505f2b77cbbdfb15137d5efdfade09156961f5277149f5e344"},
+    {file = "coverage-7.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9ad38204887349853d7c313f53a7b1c210ce138c73859e925bc4e5d8fc18e7"},
+    {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:53669b79f3d599da95a0afbef039ac0fadbb236532feb042c534fbb81b1a4e40"},
+    {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:614f1f98b84eb256e4f35e726bfe5ca82349f8dfa576faabf8a49ca09e630086"},
+    {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f1a317fdf5c122ad642db8a97964733ab7c3cf6009e1a8ae8821089993f175ff"},
+    {file = "coverage-7.3.1-cp38-cp38-win32.whl", hash = "sha256:defbbb51121189722420a208957e26e49809feafca6afeef325df66c39c4fdb3"},
+    {file = "coverage-7.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:f4f456590eefb6e1b3c9ea6328c1e9fa0f1006e7481179d749b3376fc793478e"},
+    {file = "coverage-7.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f12d8b11a54f32688b165fd1a788c408f927b0960984b899be7e4c190ae758f1"},
+    {file = "coverage-7.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f09195dda68d94a53123883de75bb97b0e35f5f6f9f3aa5bf6e496da718f0cb6"},
+    {file = "coverage-7.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6601a60318f9c3945be6ea0f2a80571f4299b6801716f8a6e4846892737ebe4"},
+    {file = "coverage-7.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07d156269718670d00a3b06db2288b48527fc5f36859425ff7cec07c6b367745"},
+    {file = "coverage-7.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:636a8ac0b044cfeccae76a36f3b18264edcc810a76a49884b96dd744613ec0b7"},
+    {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5d991e13ad2ed3aced177f524e4d670f304c8233edad3210e02c465351f785a0"},
+    {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:586649ada7cf139445da386ab6f8ef00e6172f11a939fc3b2b7e7c9082052fa0"},
+    {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4aba512a15a3e1e4fdbfed2f5392ec221434a614cc68100ca99dcad7af29f3f8"},
+    {file = "coverage-7.3.1-cp39-cp39-win32.whl", hash = "sha256:6bc6f3f4692d806831c136c5acad5ccedd0262aa44c087c46b7101c77e139140"},
+    {file = "coverage-7.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:553d7094cb27db58ea91332e8b5681bac107e7242c23f7629ab1316ee73c4981"},
+    {file = "coverage-7.3.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:220eb51f5fb38dfdb7e5d54284ca4d0cd70ddac047d750111a68ab1798945194"},
+    {file = "coverage-7.3.1.tar.gz", hash = "sha256:6cb7fe1581deb67b782c153136541e20901aa312ceedaf1467dcb35255787952"},
+]
+
+[package.dependencies]
+tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
+
+[package.extras]
+toml = ["tomli"]
+
 [[package]]
 name = "cycler"
 version = "0.11.0"
@@ -299,6 +370,20 @@ MarkupSafe = ">=2.0"
 [package.extras]
 i18n = ["Babel (>=2.7)"]
 
+[[package]]
+name = "jsonpath-ng"
+version = "1.6.0"
+description = "A final implementation of JSONPath for Python that aims to be standard compliant, including arithmetic and binary comparison operators and providing clear AST for metaprogramming."
+optional = false
+python-versions = "*"
+files = [
+    {file = "jsonpath-ng-1.6.0.tar.gz", hash = "sha256:5483f8e9d74c39c9abfab554c070ae783c1c8cbadf5df60d561bc705ac68a07e"},
+    {file = "jsonpath_ng-1.6.0-py3-none-any.whl", hash = "sha256:6fd04833412c4b3d9299edf369542f5e67095ca84efa17cbb7f06a34958adc9f"},
+]
+
+[package.dependencies]
+ply = "*"
+
 [[package]]
 name = "kiwisolver"
 version = "1.4.5"
@@ -591,6 +676,16 @@ files = [
     {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
     {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
     {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
+    {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
+    {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
+    {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
+    {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
+    {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
+    {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
+    {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
+    {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
+    {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
+    {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
     {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
     {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
     {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
@@ -875,6 +970,17 @@ files = [
 [package.extras]
 dev = ["pre-commit", "tox"]
 
+[[package]]
+name = "ply"
+version = "3.11"
+description = "Python Lex & Yacc"
+optional = false
+python-versions = "*"
+files = [
+    {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"},
+    {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"},
+]
+
 [[package]]
 name = "py"
 version = "1.11.0"
@@ -954,6 +1060,24 @@ wcwidth = "*"
 checkqa-mypy = ["mypy (==v0.761)"]
 testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
 
+[[package]]
+name = "pytest-cov"
+version = "4.1.0"
+description = "Pytest plugin for measuring coverage."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
+    {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
+]
+
+[package.dependencies]
+coverage = {version = ">=5.2.1", extras = ["toml"]}
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
+
 [[package]]
 name = "python-dateutil"
 version = "2.8.2"
@@ -981,6 +1105,65 @@ files = [
 [package.dependencies]
 lxml = ">=2.3.2"
 
+[[package]]
+name = "pyyaml"
+version = "6.0.1"
+description = "YAML parser and emitter for Python"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+    {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+    {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+    {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
+    {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
+    {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
+    {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
+    {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
+    {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+    {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
+    {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
+]
+
 [[package]]
 name = "six"
 version = "1.16.0"
@@ -1138,4 +1321,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "e723fd1b09be701d0636c320b669440206a58ce8c12cbe4e7ca0eccafdddf2c2"
+content-hash = "ae8d5253b2d2da49ee13f1b77e38e7468370f9ac3e88239fb1f4920777dc7d5a"
diff --git a/pyproject.toml b/pyproject.toml
index c735b53..899f706 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,11 +22,14 @@ jinja2 = "^3.1.2"
 python = "^3.8"
 python-docx = "^0.8.11"
 matplotlib = "^3.5.1"
+pyyaml = "^6.0.1"
+jsonpath-ng = "^1.6.0"
 
 [tool.poetry.dev-dependencies]
 black = "^22.3.0"
 pylint = "^2.13.8"
 pytest = "^5.2"
+pytest-cov = "^4.1.0"
 
 [tool.poetry.scripts]
 sarif = "sarif.cmdline.main:main"
diff --git a/sarif/cmdline/main.py b/sarif/cmdline/main.py
index fa25454..ea8c780 100644
--- a/sarif/cmdline/main.py
+++ b/sarif/cmdline/main.py
@@ -8,6 +8,7 @@
 import sys
 
 from sarif import loader, sarif_file
+from sarif.filter.general_filter import load_filter_file
 
 from sarif.operations import (
     blame_op,
@@ -92,13 +93,23 @@ def _create_arg_parser():
             "--output", "-o", type=str, metavar="FILE", help="Output file"
         )
 
-    for cmd in ["codeclimate", "copy", "csv", "diff", "summary", "html", "emacs", "trend", "word"]:
+    for cmd in [
+        "codeclimate",
+        "copy",
+        "csv",
+        "diff",
+        "summary",
+        "html",
+        "emacs",
+        "trend",
+        "word",
+    ]:
         subparser[cmd].add_argument(
-            "--blame-filter",
+            "--filter",
             "-b",
             type=str,
             metavar="FILE",
-            help="Specify the blame filter file to apply.  See README for format.",
+            help="Specify the filter file to apply.  See README for format.",
         )
 
     # Command-specific options
@@ -204,66 +215,10 @@ def _check(input_files: sarif_file.SarifFileSet, check_level):
     return ret
 
 
-def _load_blame_filter_file(file_path):
-    filter_description = os.path.basename(file_path)
-    include_substrings = []
-    include_regexps = []
-    exclude_substrings = []
-    exclude_regexps = []
-    try:
-        with open(file_path, encoding="utf-8") as file_in:
-            for line in file_in.readlines():
-                if line.startswith("\ufeff"):
-                    # Strip byte order mark
-                    line = line[1:]
-                lstrip = line.strip()
-                if lstrip.startswith("#"):
-                    # Ignore comment lines
-                    continue
-                pattern_spec = None
-                is_include = True
-                if lstrip.startswith("description:"):
-                    filter_description = lstrip[12:].strip()
-                elif lstrip.startswith("+: "):
-                    is_include = True
-                    pattern_spec = lstrip[3:].strip()
-                elif lstrip.startswith("-: "):
-                    is_include = False
-                    pattern_spec = lstrip[3:].strip()
-                else:
-                    is_include = True
-                    pattern_spec = lstrip
-                if pattern_spec:
-                    pattern_spec_len = len(pattern_spec)
-                    if (
-                        pattern_spec_len > 2
-                        and pattern_spec.startswith("/")
-                        and pattern_spec.endswith("/")
-                    ):
-                        (include_regexps if is_include else exclude_regexps).append(
-                            pattern_spec[1 : pattern_spec_len - 1]
-                        )
-                    else:
-                        (
-                            include_substrings if is_include else exclude_substrings
-                        ).append(pattern_spec)
-    except UnicodeDecodeError as error:
-        raise IOError(
-            f"Cannot read blame filter file {file_path}: not UTF-8 encoded?"
-        ) from error
-    return (
-        filter_description,
-        include_substrings,
-        include_regexps,
-        exclude_substrings,
-        exclude_regexps,
-    )
-
-
-def _init_blame_filtering(input_files, args):
-    if args.blame_filter:
-        filters = _load_blame_filter_file(args.blame_filter)
-        input_files.init_blame_filter(*filters)
+def _init_filtering(input_files, args):
+    if args.filter:
+        filters = load_filter_file(args.filter)
+        input_files.init_general_filter(*filters)
 
 
 def _init_path_prefix_stripping(input_files, args, strip_by_default):
@@ -337,7 +292,7 @@ def _codeclimate(args):
     input_files = loader.load_sarif_files(*args.files_or_dirs)
     input_files.init_default_line_number_1()
     _init_path_prefix_stripping(input_files, args, strip_by_default=False)
-    _init_blame_filtering(input_files, args)
+    _init_filtering(input_files, args)
     (output, multiple_file_output) = _prepare_output(input_files, args.output, ".json")
     codeclimate_op.generate(input_files, output, multiple_file_output)
     return _check(input_files, args.check)
@@ -345,7 +300,7 @@ def _codeclimate(args):
 
 def _copy(args):
     input_files = loader.load_sarif_files(*args.files_or_dirs)
-    _init_blame_filtering(input_files, args)
+    _init_filtering(input_files, args)
     output = args.output or "out.sarif"
     output_sarif_file_set = copy_op.generate_sarif(
         input_files,
@@ -361,7 +316,7 @@ def _csv(args):
     input_files = loader.load_sarif_files(*args.files_or_dirs)
     input_files.init_default_line_number_1()
     _init_path_prefix_stripping(input_files, args, strip_by_default=False)
-    _init_blame_filtering(input_files, args)
+    _init_filtering(input_files, args)
     (output, multiple_file_output) = _prepare_output(input_files, args.output, ".csv")
     csv_op.generate_csv(input_files, output, multiple_file_output)
     return _check(input_files, args.check)
@@ -370,8 +325,8 @@ def _csv(args):
 def _diff(args):
     original_sarif = loader.load_sarif_files(args.old_file_or_dir[0])
     new_sarif = loader.load_sarif_files(args.new_file_or_dir[0])
-    _init_blame_filtering(original_sarif, args)
-    _init_blame_filtering(new_sarif, args)
+    _init_filtering(original_sarif, args)
+    _init_filtering(new_sarif, args)
     return diff_op.print_diff(original_sarif, new_sarif, args.output, args.check)
 
 
@@ -379,7 +334,7 @@ def _html(args):
     input_files = loader.load_sarif_files(*args.files_or_dirs)
     input_files.init_default_line_number_1()
     _init_path_prefix_stripping(input_files, args, strip_by_default=True)
-    _init_blame_filtering(input_files, args)
+    _init_filtering(input_files, args)
     (output, multiple_file_output) = _prepare_output(input_files, args.output, ".html")
     html_op.generate_html(input_files, args.image, output, multiple_file_output)
     return _check(input_files, args.check)
@@ -389,7 +344,7 @@ def _emacs(args):
     input_files = loader.load_sarif_files(*args.files_or_dirs)
     input_files.init_default_line_number_1()
     _init_path_prefix_stripping(input_files, args, strip_by_default=True)
-    _init_blame_filtering(input_files, args)
+    _init_filtering(input_files, args)
     (output, multiple_file_output) = _prepare_output(input_files, args.output, ".txt")
     emacs_op.generate_compile(input_files, output, multiple_file_output)
     return _check(input_files, args.check)
@@ -413,7 +368,7 @@ def _ls(args):
 
 def _summary(args):
     input_files = loader.load_sarif_files(*args.files_or_dirs)
-    _init_blame_filtering(input_files, args)
+    _init_filtering(input_files, args)
     (output, multiple_file_output) = (None, False)
     if args.output:
         (output, multiple_file_output) = _prepare_output(
@@ -426,7 +381,7 @@ def _summary(args):
 def _trend(args):
     input_files = loader.load_sarif_files(*args.files_or_dirs)
     input_files.init_default_line_number_1()
-    _init_blame_filtering(input_files, args)
+    _init_filtering(input_files, args)
     if args.output:
         _ensure_dir(os.path.dirname(args.output))
         output = args.output
@@ -453,7 +408,7 @@ def _word(args):
     input_files = loader.load_sarif_files(*args.files_or_dirs)
     input_files.init_default_line_number_1()
     _init_path_prefix_stripping(input_files, args, strip_by_default=True)
-    _init_blame_filtering(input_files, args)
+    _init_filtering(input_files, args)
     (output, multiple_file_output) = _prepare_output(input_files, args.output, ".docx")
     word_op.generate_word_docs_from_sarif_inputs(
         input_files, args.image, output, multiple_file_output
@@ -469,7 +424,7 @@ def _word(args):
     "codeclimate": {
         "fn": _codeclimate,
         "desc": "Write a JSON representation in Code Climate format of SARIF file(s) "
-                "for viewing as a Code Quality report in GitLab UI",
+        "for viewing as a Code Quality report in GitLab UI",
     },
     "copy": {
         "fn": _copy,
diff --git a/sarif/filter/__init__.py b/sarif/filter/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sarif/filter/filter_stats.py b/sarif/filter/filter_stats.py
new file mode 100644
index 0000000..ec2be81
--- /dev/null
+++ b/sarif/filter/filter_stats.py
@@ -0,0 +1,95 @@
+import datetime
+
+
+class FilterStats:
+    """
+    Statistics that record the outcome of a a filter.
+    """
+
+    def __init__(self, filter_description):
+        self.filter_description = filter_description
+        # Filter stats can also be loaded from a file created by `sarif copy`.
+        self.rehydrated = False
+        self.filter_datetime = None
+        self.filtered_in_result_count = 0
+        self.filtered_out_result_count = 0
+        self.missing_property_count = 0
+
+    def reset_counters(self):
+        """
+        Zero all the counters.
+        """
+        self.filter_datetime = datetime.datetime.now()
+        self.filtered_in_result_count = 0
+        self.filtered_out_result_count = 0
+        self.missing_property_count = 0
+
+    def add(self, other_filter_stats):
+        """
+        Add another set of filter stats to my totals.
+        """
+        if other_filter_stats:
+            if other_filter_stats.filter_description and (
+                other_filter_stats.filter_description != self.filter_description
+            ):
+                self.filter_description += f", {other_filter_stats.filter_description}"
+            self.filtered_in_result_count += other_filter_stats.filtered_in_result_count
+            self.filtered_out_result_count += (
+                other_filter_stats.filtered_out_result_count
+            )
+            self.missing_property_count += other_filter_stats.missing_property_count
+
+    def __str__(self):
+        """
+        Automatic to_string()
+        """
+        return self.to_string()
+
+    def to_string(self):
+        """
+        Generate a summary string for these filter stats.
+        """
+        ret = f"'{self.filter_description}'"
+        if self.filter_datetime:
+            ret += " at "
+            ret += self.filter_datetime.strftime("%c")
+        ret += (
+            f": {self.filtered_out_result_count} filtered out, "
+            f"{self.filtered_in_result_count} passed the filter"
+        )
+        if self.missing_property_count:
+            ret += (
+                f", {self.missing_property_count} included by default "
+                "for lacking data to filter"
+            )
+
+        return ret
+
+    def to_json_camel_case(self):
+        """
+        Generate filter stats as JSON using camelCase naming,
+        to fit with SARIF standard section 3.8.1 (Property Bags).
+        """
+        return {
+            "filter": self.filter_description,
+            "in": self.filtered_in_result_count,
+            "out": self.filtered_out_result_count,
+            "default": {
+                "noProperty": self.missing_property_count,
+            },
+        }
+
+
+def load_filter_stats_from_json(json_data):
+    """
+    Load filter stats from a SARIF file property bag using camelCase naming
+    as per SARIF standard section 3.8.1 (Property Bags).
+    """
+    ret = None
+    if json_data:
+        ret = FilterStats(json_data["filter"])
+        ret.rehydrated = True
+        ret.filtered_in_result_count = json_data.get("in", 0)
+        ret.filtered_out_result_count = json_data.get("out", 0)
+        ret.missing_property_count = json_data.get("default", {}).get("noProperty", 0)
+    return ret
diff --git a/sarif/filter/general_filter.py b/sarif/filter/general_filter.py
new file mode 100644
index 0000000..b9fcb48
--- /dev/null
+++ b/sarif/filter/general_filter.py
@@ -0,0 +1,227 @@
+import os
+import re
+from typing import Optional, List
+
+import copy
+import jsonpath_ng.ext
+import yaml
+
+from sarif.filter.filter_stats import FilterStats, load_filter_stats_from_json
+
+# Commonly used properties can be specified using shortcuts
+# instead of full JSON path
+FILTER_SHORTCUTS = {
+    "author": "properties.blame.author",
+    "author-mail": "properties.blame.author-mail",
+    "committer": "properties.blame.committer",
+    "committer-mail": "properties.blame.committer-mail",
+    "location": "locations[*].physicalLocation.artifactLocation.uri",
+    "rule": "ruleId",
+    "suppression": "suppressions[*].kind",
+}
+
+# Some properties can have specific shortcuts to make it easier to write filters
+# For example a file location can be specified using wildcards
+FIELDS_REGEX_SHORTCUTS = {"uri": {"**": ".*", "*": "[^/]*", "?": "."}}
+
+# Default configuration for all filters
+DEFAULT_CONFIGURATION = {
+    "default-include": True,
+}
+
+
+def get_filter_function(filter_spec):
+    if filter_spec:
+        filter_len = len(filter_spec)
+        if filter_len > 2 and filter_spec.startswith("/") and filter_spec.endswith("/"):
+            regex = filter_spec[1:-1]
+            return lambda value: re.search(regex, value, re.IGNORECASE)
+        else:
+            substring = filter_spec
+            # substring can be empty, in this case "in" returns true
+            # and only existence of the property checked.
+            return lambda value: substring in value
+    return lambda value: True
+
+
+def _convert_glob_to_regex(property_name, property_value_spec):
+    # skip if property_value_spec is a regex
+    if property_value_spec and not (
+        property_value_spec.startswith("/") and property_value_spec.endswith("/")
+    ):
+        # get last component of property name
+        last_component = property_name.split(".")[-1]
+        if last_component in FIELDS_REGEX_SHORTCUTS:
+            shortcuts = FIELDS_REGEX_SHORTCUTS[last_component]
+            rx = re.compile("|".join(map(re.escape, shortcuts.keys())))
+            property_value_spec = rx.sub(
+                lambda match: shortcuts[match.group(0)], property_value_spec
+            )
+
+            return f"/{property_value_spec}/"
+    return property_value_spec
+
+
+class GeneralFilter:
+    """
+    Class that implements filtering.
+    """
+
+    def __init__(self):
+        self.filter_stats = None
+        self.include_filters = {}
+        self.apply_inclusion_filter = False
+        self.exclude_filters = {}
+        self.apply_exclusion_filter = False
+        self.configuration = DEFAULT_CONFIGURATION
+
+    def init_filter(
+        self, filter_description, configuration, include_filters, exclude_filters
+    ):
+        """
+        Initialise the filter with the given filter patterns.
+        """
+        self.filter_stats = FilterStats(filter_description)
+        self.include_filters = include_filters
+        self.apply_inclusion_filter = len(include_filters) > 0
+        self.exclude_filters = exclude_filters
+        self.apply_exclusion_filter = len(exclude_filters) > 0
+        self.configuration.update(configuration)
+
+    def rehydrate_filter_stats(self, dehydrated_filter_stats, filter_datetime):
+        """
+        Restore filter stats from the SARIF file directly,
+        where they were recorded when the filter was previously run.
+
+        Note that if init_filter is called,
+        these rehydrated stats are discarded.
+        """
+        self.filter_stats = load_filter_stats_from_json(dehydrated_filter_stats)
+        self.filter_stats.filter_datetime = filter_datetime
+
+    def _zero_counts(self):
+        if self.filter_stats:
+            self.filter_stats.reset_counters()
+
+    def _filter_append(self, filtered_results: List[dict], result: dict):
+        # Remove any existing filter log on the result
+        result.setdefault("properties", {}).pop("filtered", None)
+
+        included_stats = None
+        if self.apply_inclusion_filter:
+            included_stats = self._filter_result(result, self.include_filters)
+            if not included_stats["matchedFilter"]:
+                return
+
+        if self.apply_exclusion_filter:
+            excluded_stats = self._filter_result(result, self.exclude_filters)
+            if excluded_stats["matchedFilter"]:
+                self.filter_stats.filtered_out_result_count += 1
+                return
+
+        if included_stats["state"] == "included":
+            self.filter_stats.filtered_in_result_count += 1
+        else:
+            self.filter_stats.missing_property_count += 1
+        included_stats["filter"] = self.filter_stats.filter_description
+        result["properties"]["filtered"] = included_stats
+
+        filtered_results.append(result)
+
+    def _filter_result(self, result: dict, filters: List[str]) -> dict:
+        matched_filters = []
+        warnings = []
+        if filters:
+            # filters contains rules which treated as OR.
+            # if any rule matches, the record is selected.
+            for filter_spec in filters:
+                # filter_spec contains rules which treated as AND.
+                # all rules must match to select the record.
+                matched = True
+                for prop_path, prop_value_spec in filter_spec.items():
+                    resolved_prop_path = FILTER_SHORTCUTS.get(prop_path, prop_path)
+                    jsonpath_expr = jsonpath_ng.ext.parse(resolved_prop_path)
+                    filter_configuration = self.configuration
+
+                    # if prop_value_spec is a dict, update filter configuration from it
+                    if isinstance(prop_value_spec, dict):
+                        filter_configuration = copy.deepcopy(self.configuration)
+                        filter_configuration.update(prop_value_spec)
+                        # actual value for the filter is in "value" key
+                        prop_value_spec = prop_value_spec.get("value", None)
+
+                    found_results = jsonpath_expr.find(result)
+                    if found_results:
+                        value = found_results[0].value
+                        value_spec = _convert_glob_to_regex(
+                            resolved_prop_path, prop_value_spec
+                        )
+                        filter_function = get_filter_function(value_spec)
+                        if filter_function(value):
+                            continue
+                    else:
+                        # property to filter on is not found.
+                        # if "default-include" is true, include the "result" with a warning.
+                        if filter_configuration.get("default-include", True):
+                            warnings.append(
+                                f"Field '{prop_path}' is missing but the result included as default-include is true"
+                            )
+                            continue
+                    matched = False
+                    break
+                if matched:
+                    matched_filters.append(filter_spec)
+                    break
+
+        stats = {
+            "state": "included",
+            "matchedFilter": matched_filters,
+        }
+
+        if warnings:
+            stats.update(
+                {
+                    "state": "default",
+                    "warnings": warnings,
+                }
+            )
+
+        return stats
+
+    def filter_results(self, results: List[dict]) -> List[dict]:
+        """
+        Apply this filter to a list of results,
+        return the results that pass the filter
+        and as a side-effect, update the filter stats.
+        """
+        if self.apply_inclusion_filter or self.apply_exclusion_filter:
+            self._zero_counts()
+            ret = []
+            for result in results:
+                self._filter_append(ret, result)
+            return ret
+        # No inclusion or exclusion patterns
+        return results
+
+    def get_filter_stats(self) -> Optional[FilterStats]:
+        """
+        Get the statistics from running this filter.
+        """
+        return self.filter_stats
+
+
+def load_filter_file(file_path):
+    """
+    Load a YAML filter file, return the filter description and the filters.
+    """
+    try:
+        file_name = os.path.basename(file_path)
+        with open(file_path, encoding="utf-8") as file_in:
+            yaml_content = yaml.safe_load(file_in)
+            filter_description = yaml_content.get("description", file_name)
+            configuration = yaml_content.get("configuration", {})
+            include_filters = yaml_content.get("include", {})
+            exclude_filters = yaml_content.get("exclude", {})
+    except yaml.YAMLError as error:
+        raise IOError(f"Cannot read filter file {file_path}") from error
+    return filter_description, configuration, include_filters, exclude_filters
diff --git a/sarif/operations/codeclimate_op.py b/sarif/operations/codeclimate_op.py
index 44ab4dc..c64276e 100644
--- a/sarif/operations/codeclimate_op.py
+++ b/sarif/operations/codeclimate_op.py
@@ -8,12 +8,7 @@
 
 from sarif.sarif_file import SarifFileSet
 
-_SEVERITIES = {
-    "none": "info",
-    "note": "info",
-    "warning": "minor",
-    "error": "major"
-}
+_SEVERITIES = {"none": "info", "note": "info", "warning": "minor", "error": "major"}
 
 
 def generate(input_files: SarifFileSet, output: str, output_multiple_files: bool):
@@ -61,30 +56,29 @@ def _write_to_json(list_of_errors, output_file):
         severity = _SEVERITIES.get(record.get("Severity", "warning"), "minor")
 
         # split Code value to extract error ID and description
-        rule = record["Code"].split(' ', 1)[0]
-        description = record["Code"][len(rule)+1:]
+        rule = record["Code"].split(" ", 1)[0]
+        description = record["Code"][len(rule) + 1 :]
 
         path = record["Location"]
         line = record["Line"]
 
-        fingerprint = hashlib.md5(f"{description} {path} ${line}`]".encode()).hexdigest()
+        fingerprint = hashlib.md5(
+            f"{description} {path} ${line}`]".encode()
+        ).hexdigest()
 
         # "categories" property is not used in GitLab but marked as "required" in Code Climate spec.
         # There is no easy way to determine a category so the fixed value is set.
-        content.append({
-            "type": "issue",
-            "check_name": rule,
-            "description": description,
-            "categories": ["Bug Risk"],
-            "location": {
-                "path": path,
-                "lines": {
-                    "begin": line
-                }
-            },
-            "severity": severity,
-            "fingerprint": fingerprint
-        })
+        content.append(
+            {
+                "type": "issue",
+                "check_name": rule,
+                "description": description,
+                "categories": ["Bug Risk"],
+                "location": {"path": path, "lines": {"begin": line}},
+                "severity": severity,
+                "fingerprint": fingerprint,
+            }
+        )
 
     with open(output_file, "w", encoding="utf-8") as file_out:
         json.dump(content, file_out, indent=4)
diff --git a/sarif/sarif_file.py b/sarif/sarif_file.py
index c2d3b15..ba1f5f4 100644
--- a/sarif/sarif_file.py
+++ b/sarif/sarif_file.py
@@ -9,12 +9,16 @@
 import re
 from typing import Dict, Iterator, List, Optional, Tuple
 
+from sarif.filter.general_filter import GeneralFilter
+from sarif.filter.filter_stats import FilterStats
+
 SARIF_SEVERITIES = ["error", "warning", "note"]
 
 BASIC_RECORD_ATTRIBUTES = ["Tool", "Severity", "Code", "Location", "Line"]
 BLAME_RECORD_ATTRIBUTES = ["Author"]
 
-# Standard time format for filenames, e.g. `20211012T110000Z` (not part of the SARIF standard).
+# Standard time format for filenames, e.g. `20211012T110000Z`
+# (not part of the SARIF standard).
 # Can obtain from bash via `date +"%Y%m%dT%H%M%SZ"``
 DATETIME_FORMAT = "%Y%m%dT%H%M%SZ"
 DATETIME_REGEX = r"\d{8}T\d{6}Z"
@@ -103,112 +107,6 @@ def get_record_headings(with_blame) -> List[str]:
     )
 
 
-class FilterStats:
-    """
-    Statistics that record the outcome of a a filter.
-    """
-
-    def __init__(self, filter_description):
-        self.filter_description = filter_description
-        # Filter stats can also be loaded from a file created by `sarif copy`.
-        self.rehydrated = False
-        self.filter_datetime = None
-        self.filtered_in_result_count = 0
-        self.filtered_out_result_count = 0
-        self.missing_blame_count = 0
-        self.unconvincing_line_number_count = 0
-
-    def reset_counters(self):
-        """
-        Zero all the counters.
-        """
-        self.filter_datetime = datetime.datetime.now()
-        self.filtered_in_result_count = 0
-        self.filtered_out_result_count = 0
-        self.missing_blame_count = 0
-        self.unconvincing_line_number_count = 0
-
-    def add(self, other_filter_stats):
-        """
-        Add another set of filter stats to my totals.
-        """
-        if other_filter_stats:
-            if other_filter_stats.filter_description and (
-                other_filter_stats.filter_description != self.filter_description
-            ):
-                self.filter_description += f", {other_filter_stats.filter_description}"
-            self.filtered_in_result_count += other_filter_stats.filtered_in_result_count
-            self.filtered_out_result_count += (
-                other_filter_stats.filtered_out_result_count
-            )
-            self.missing_blame_count += other_filter_stats.missing_blame_count
-            self.unconvincing_line_number_count += (
-                other_filter_stats.unconvincing_line_number_count
-            )
-
-    def __str__(self):
-        """
-        Automatic to_string()
-        """
-        return self.to_string()
-
-    def to_string(self):
-        """
-        Generate a summary string for these filter stats.
-        """
-        ret = f"'{self.filter_description}'"
-        if self.filter_datetime:
-            ret += " at "
-            ret += self.filter_datetime.strftime("%c")
-        ret += (
-            f": {self.filtered_out_result_count} filtered out, "
-            f"{self.filtered_in_result_count} passed the filter"
-        )
-        if self.unconvincing_line_number_count:
-            ret += (
-                f", {self.unconvincing_line_number_count} included by default "
-                "for lacking line number information"
-            )
-        if self.missing_blame_count:
-            ret += (
-                f", {self.missing_blame_count} included by default "
-                "for lacking blame data to filter"
-            )
-        return ret
-
-    def to_json_camel_case(self):
-        """
-        Generate filter stats as JSON using camelCase naming, to fit with SARIF standard section
-        3.8.1 (Property Bags).
-        """
-        return {
-            "filter": self.filter_description,
-            "in": self.filtered_in_result_count,
-            "out": self.filtered_out_result_count,
-            "default": {
-                "noLineNumber": self.unconvincing_line_number_count,
-                "noBlame": self.missing_blame_count,
-            },
-        }
-
-
-def load_filter_stats_from_json_camel_case(json_data):
-    """
-    Load filter stats from a SARIF file property bag
-    """
-    ret = None
-    if json_data:
-        ret = FilterStats(json_data["filter"])
-        ret.rehydrated = True
-        ret.filtered_in_result_count = json_data.get("in", 0)
-        ret.filtered_out_result_count = json_data.get("out", 0)
-        ret.unconvincing_line_number_count = json_data.get("default", {}).get(
-            "noLineNumber", 0
-        )
-        ret.missing_blame_count = json_data.get("default", {}).get("noBlame", 0)
-    return ret
-
-
 def _add_filter_stats(accumulator, filter_stats):
     if filter_stats:
         if accumulator:
@@ -226,159 +124,6 @@ def _get_author_mail_from_blame_info(blame_info):
     )
 
 
-class _BlameFilter:
-    """
-    Class that implements blame filtering.
-    """
-
-    def __init__(self):
-        self.filter_stats = None
-        self.include_substrings = None
-        self.include_regexes = None
-        self.apply_inclusion_filter = False
-        self.exclude_substrings = None
-        self.exclude_regexes = None
-        self.apply_exclusion_filter = False
-
-    def init_blame_filter(
-        self,
-        filter_description,
-        include_substrings,
-        include_regexes,
-        exclude_substrings,
-        exclude_regexes,
-    ):
-        """
-        Initialise the blame filter with the given filter patterns.
-        """
-        self.filter_stats = FilterStats(filter_description)
-        self.include_substrings = (
-            [s.upper().strip() for s in include_substrings]
-            if include_substrings
-            else None
-        )
-        self.include_regexes = include_regexes[:] if include_regexes else None
-        self.apply_inclusion_filter = bool(
-            self.include_substrings or self.include_regexes
-        )
-        self.exclude_substrings = (
-            [s.upper().strip() for s in exclude_substrings]
-            if exclude_substrings
-            else None
-        )
-        self.exclude_regexes = exclude_regexes[:] if exclude_regexes else None
-        self.apply_exclusion_filter = bool(
-            self.exclude_substrings or self.exclude_regexes
-        )
-
-    def rehydrate_filter_stats(self, dehydrated_filter_stats, filter_datetime):
-        """
-        Restore filter stats from the SARIF file directly, where they were recorded when the filter
-        was previously run.
-
-        Note that if init_blame_filter is called, these rehydrated stats are discarded.
-        """
-        self.filter_stats = load_filter_stats_from_json_camel_case(
-            dehydrated_filter_stats
-        )
-        self.filter_stats.filter_datetime = filter_datetime
-
-    def _zero_counts(self):
-        if self.filter_stats:
-            self.filter_stats.reset_counters()
-
-    def _check_include_result(self, author_mail):
-        author_mail_upper = author_mail.upper().strip()
-        matched_include_substrings = None
-        matched_include_regexes = None
-        if self.apply_inclusion_filter:
-            if self.include_substrings:
-                matched_include_substrings = [
-                    s for s in self.include_substrings if s in author_mail_upper
-                ]
-            if self.include_regexes:
-                matched_include_regexes = [
-                    r
-                    for r in self.include_regexes
-                    if re.search(r, author_mail, re.IGNORECASE)
-                ]
-            if (not matched_include_substrings) and (not matched_include_regexes):
-                return False
-        if self.exclude_substrings and any(
-            s in author_mail_upper for s in self.exclude_substrings
-        ):
-            return False
-        if self.exclude_regexes and any(
-            re.search(r, author_mail, re.IGNORECASE) for r in self.exclude_regexes
-        ):
-            return False
-        return {
-            "state": "included",
-            "matchedSubstring": [s.lower() for s in matched_include_substrings]
-            if matched_include_substrings
-            else [],
-            "matchedRegex": [r.lower() for r in matched_include_regexes]
-            if matched_include_regexes
-            else [],
-        }
-
-    def _filter_append(self, filtered_results, result, blame_info):
-        # Remove any existing filter log on the result
-        result.setdefault("properties", {}).pop("filtered", None)
-        author_mail = _get_author_mail_from_blame_info(blame_info)
-        if author_mail:
-            # First, check inclusion
-            included = self._check_include_result(author_mail)
-            if included:
-                self.filter_stats.filtered_in_result_count += 1
-                included["filter"] = self.filter_stats.filter_description
-                result["properties"]["filtered"] = included
-                filtered_results.append(result)
-            else:
-                (_file_path, line_number) = _read_result_location(result)
-                if line_number == "1" or not line_number:
-                    # Line number is not convincing.  Blame information may be misattributed.
-                    self.filter_stats.unconvincing_line_number_count += 1
-                    result["properties"]["filtered"] = {
-                        "filter": self.filter_stats.filter_description,
-                        "state": "default",
-                        "missing": "line",
-                    }
-                    filtered_results.append(result)
-                else:
-                    self.filter_stats.filtered_out_result_count += 1
-        else:
-            self.filter_stats.missing_blame_count += 1
-            # Result did not contain complete blame information, so don't filter it out.
-            result["properties"]["filtered"] = {
-                "filter": self.filter_stats.filter_description,
-                "state": "default",
-                "missing": "blame",
-            }
-            filtered_results.append(result)
-
-    def filter_results(self, results):
-        """
-        Apply this blame filter to a list of results, return the results that pass the filter
-        and as a side-effect, update the filter stats.
-        """
-        if self.apply_inclusion_filter or self.apply_exclusion_filter:
-            self._zero_counts()
-            ret = []
-            for result in results:
-                blame_info = result.get("properties", {}).get("blame", None)
-                self._filter_append(ret, result, blame_info)
-            return ret
-        # No inclusion or exclusion patterns
-        return results
-
-    def get_filter_stats(self) -> Optional[FilterStats]:
-        """
-        Get the statistics from running this filter.
-        """
-        return self.filter_stats
-
-
 class SarifRun:
     """
     Class to hold a run object from a SARIF file (an entry in the top-level "runs" list
@@ -392,7 +137,7 @@ def __init__(self, sarif_file_object, run_index, run_data):
         self.run_data = run_data
         self._path_prefixes_upper = None
         self._cached_records = None
-        self._filter = _BlameFilter()
+        self._filter = GeneralFilter()
         self._default_line_number = None
         conversion = run_data.get("conversion", None)
         if conversion:
@@ -459,35 +204,18 @@ def init_default_line_number_1(self):
         self._default_line_number = "1"
         self._cached_records = None
 
-    def init_blame_filter(
-        self,
-        filter_description,
-        include_substrings,
-        include_regexes,
-        exclude_substrings,
-        exclude_regexes,
+    def init_general_filter(
+        self, filter_description, configuration, include_filters, exclude_filters
     ):
         """
-        Set up blame filtering.  This is applied to the author_mail field added to the "blame"
-        property bag in each SARIF file.  Raises an error if any of the SARIF files don't contain
-        blame information.
-        If only inclusion criteria are provided, only issues matching the inclusion criteria
-        are considered.
-        If only exclusion criteria are provided, only issues not matching the exclusion criteria
-        are considered.
+        Set up general filtering.  This is applied to all properties in results array in each SARIF file.
+        If only inclusion criteria are provided, only issues matching the inclusion criteria are considered.
+        If only exclusion criteria are provided, only issues not matching the exclusion criteria are considered.
         If both are provided, only issues matching the inclusion criteria and not matching the
         exclusion criteria are considered.
-        include_substrings = substrings of author_mail to filter issues for inclusion.
-        include_regexes = regular expressions for author_mail to filter issues for inclusion.
-        exclude_substrings = substrings of author_mail to filter issues for exclusion.
-        exclude_regexes = regular expressions for author_mail to filter issues for exclusion.
-        """
-        self._filter.init_blame_filter(
-            filter_description,
-            include_substrings,
-            include_regexes,
-            exclude_substrings,
-            exclude_regexes,
+        """
+        self._filter.init_filter(
+            filter_description, configuration, include_filters, exclude_filters
         )
         # Clear the unfiltered records cached by get_records() above.
         self._cached_records = None
@@ -696,36 +424,19 @@ def init_default_line_number_1(self):
         for run in self.runs:
             run.init_default_line_number_1()
 
-    def init_blame_filter(
-        self,
-        filter_description,
-        include_substrings,
-        include_regexes,
-        exclude_substrings,
-        exclude_regexes,
+    def init_general_filter(
+        self, filter_description, configuration, include_filters, exclude_filters
     ):
         """
-        Set up blame filtering.  This is applied to the author_mail field added to the "blame"
-        property bag in each SARIF file.  Raises an error if any of the SARIF files don't contain
-        blame information.
-        If only inclusion criteria are provided, only issues matching the inclusion criteria
-        are considered.
-        If only exclusion criteria are provided, only issues not matching the exclusion criteria
-        are considered.
+        Set up general filtering.  This is applied to all properties in results array in each SARIF file.
+        If only inclusion criteria are provided, only issues matching the inclusion criteria are considered.
+        If only exclusion criteria are provided, only issues not matching the exclusion criteria are considered.
         If both are provided, only issues matching the inclusion criteria and not matching the
         exclusion criteria are considered.
-        include_substrings = substrings of author_mail to filter issues for inclusion.
-        include_regexes = regular expressions for author_mail to filter issues for inclusion.
-        exclude_substrings = substrings of author_mail to filter issues for exclusion.
-        exclude_regexes = regular expressions for author_mail to filter issues for exclusion.
         """
         for run in self.runs:
-            run.init_blame_filter(
-                filter_description,
-                include_substrings,
-                include_regexes,
-                exclude_substrings,
-                exclude_regexes,
+            run.init_general_filter(
+                filter_description, configuration, include_filters, exclude_filters
             )
 
     def get_abs_file_path(self) -> str:
@@ -914,44 +625,23 @@ def init_default_line_number_1(self):
         for input_file in self.files:
             input_file.init_default_line_number_1()
 
-    def init_blame_filter(
-        self,
-        filter_description,
-        include_substrings,
-        include_regexes,
-        exclude_substrings,
-        exclude_regexes,
+    def init_general_filter(
+        self, filter_description, configuration, include_filters, exclude_filters
     ):
         """
-        Set up blame filtering.  This is applied to the author_mail field added to the "blame"
-        property bag in each SARIF file.  Raises an error if any of the SARIF files don't contain
-        blame information.
-        If only inclusion criteria are provided, only issues matching the inclusion criteria
-        are considered.
-        If only exclusion criteria are provided, only issues not matching the exclusion criteria
-        are considered.
+        Set up general filtering.  This is applied to all properties in results array in each SARIF file.
+        If only inclusion criteria are provided, only issues matching the inclusion criteria are considered.
+        If only exclusion criteria are provided, only issues not matching the exclusion criteria are considered.
         If both are provided, only issues matching the inclusion criteria and not matching the
         exclusion criteria are considered.
-        include_substrings = substrings of author_mail to filter issues for inclusion.
-        include_regexes = regular expressions for author_mail to filter issues for inclusion.
-        exclude_substrings = substrings of author_mail to filter issues for exclusion.
-        exclude_regexes = regular expressions for author_mail to filter issues for exclusion.
         """
         for subdir in self.subdirs:
-            subdir.init_blame_filter(
-                filter_description,
-                include_substrings,
-                include_regexes,
-                exclude_substrings,
-                exclude_regexes,
+            subdir.init_general_filter(
+                filter_description, configuration, include_filters, exclude_filters
             )
         for input_file in self.files:
-            input_file.init_blame_filter(
-                filter_description,
-                include_substrings,
-                include_regexes,
-                exclude_substrings,
-                exclude_regexes,
+            input_file.init_general_filter(
+                filter_description, configuration, include_filters, exclude_filters
             )
 
     def add_dir(self, sarif_file_set):


Test patch:
diff --git a/tests/test_general_filter.py b/tests/test_general_filter.py
new file mode 100644
index 0000000..40c5d6d
--- /dev/null
+++ b/tests/test_general_filter.py
@@ -0,0 +1,325 @@
+import pytest
+from sarif.filter.general_filter import GeneralFilter, load_filter_file
+from sarif.filter.filter_stats import load_filter_stats_from_json
+
+
+class TestGeneralFilter:
+    def test_init_filter(self):
+        gf = GeneralFilter()
+
+        gf.init_filter(
+            "test filter",
+            {},
+            [{"author": "John Doe"}],
+            [{"suppression": "not a suppression"}],
+        )
+        assert gf.filter_stats.filter_description == "test filter"
+        assert gf.include_filters == [{"author": "John Doe"}]
+        assert gf.apply_inclusion_filter is True
+        assert gf.exclude_filters == [{"suppression": "not a suppression"}]
+        assert gf.apply_exclusion_filter is True
+
+    def test_rehydrate_filter_stats(self):
+        gf = GeneralFilter()
+        dehydrated_filter_stats = {
+            "filter": "test filter",
+            "in": 10,
+            "out": 5,
+            "default": {"noProperty": 3},
+        }
+        gf.rehydrate_filter_stats(dehydrated_filter_stats, "2022-01-01T00:00:00Z")
+        assert gf.filter_stats.filtered_in_result_count == 10
+        assert gf.filter_stats.filtered_out_result_count == 5
+        assert gf.filter_stats.missing_property_count == 3
+        assert gf.filter_stats.filter_datetime == "2022-01-01T00:00:00Z"
+
+    def test_zero_counts(self):
+        gf = GeneralFilter()
+        gf.filter_stats = load_filter_stats_from_json(
+            {"filter": "test filter", "in": 10, "out": 5, "default": {"noProperty": 3}}
+        )
+
+        gf._zero_counts()
+        assert gf.filter_stats.filtered_in_result_count == 0
+        assert gf.filter_stats.filtered_out_result_count == 0
+        assert gf.filter_stats.missing_property_count == 0
+
+    def test_filter_append_include(self):
+        general_filter = GeneralFilter()
+        general_filter.init_filter("test filter", {}, [{"ruleId": "test-rule"}], [])
+        result = {"ruleId": "test-rule"}
+
+        filtered_results = general_filter.filter_results([result])
+        assert len(filtered_results) == 1
+        assert filtered_results[0] == result
+        assert filtered_results[0]["properties"]["filtered"]["state"] == "included"
+        assert general_filter.filter_stats.filtered_in_result_count == 1
+        assert general_filter.filter_stats.filtered_out_result_count == 0
+        assert general_filter.filter_stats.missing_property_count == 0
+
+    def test_filter_append_exclude(self):
+        general_filter = GeneralFilter()
+        general_filter.init_filter("test filter", {}, [], [{"level": "error"}])
+        result = {"level": "error"}
+
+        filtered_results = general_filter.filter_results([result])
+        assert len(filtered_results) == 0
+        assert "filtered" not in result
+        assert general_filter.filter_stats.filtered_in_result_count == 0
+        assert general_filter.filter_stats.filtered_out_result_count == 1
+        assert general_filter.filter_stats.missing_property_count == 0
+
+    def test_filter_append_no_filters(self):
+        general_filter = GeneralFilter()
+        general_filter.init_filter("test filter", {}, [], [])
+        result = {"ruleId": "test-rule"}
+
+        filtered_results = general_filter.filter_results([result])
+        assert len(filtered_results) == 1
+        assert filtered_results[0] == result
+        assert "filtered" not in result
+
+    def test_filter_results_match(self):
+        general_filter = GeneralFilter()
+        general_filter.init_filter(
+            "test filter", {}, [{"ruleId": "test-rule"}, {"level": "error"}], []
+        )
+        result = {"ruleId": "test-rule", "level": "error"}
+
+        filtered_results = general_filter.filter_results([result])
+        assert len(filtered_results) == 1
+        assert filtered_results[0] == result
+        assert filtered_results[0]["properties"]["filtered"]["state"] == "included"
+        assert filtered_results[0]["properties"]["filtered"]["matchedFilter"] == [
+            {"ruleId": "test-rule"}
+        ]
+        assert "warnings" not in filtered_results[0]["properties"]["filtered"]
+        assert general_filter.filter_stats.filtered_in_result_count == 1
+        assert general_filter.filter_stats.filtered_out_result_count == 0
+        assert general_filter.filter_stats.missing_property_count == 0
+
+    def test_filter_results_no_match(self):
+        general_filter = GeneralFilter()
+        general_filter.init_filter(
+            "test filter", {}, [{"ruleId": "other-rule"}, {"level": "warning"}], []
+        )
+        result = {"ruleId": "test-rule", "level": "error"}
+
+        filtered_results = general_filter.filter_results([result])
+        assert len(filtered_results) == 0
+
+    def test_filter_results_regex(self):
+        general_filter = GeneralFilter()
+        rule = {"properties.blame.author-mail": "/myname\\..*\\.com/"}
+        general_filter.init_filter(
+            "test filter",
+            {},
+            [rule],
+            [],
+        )
+        result = {
+            "ruleId": "test-rule",
+            "properties": {"blame": {"author-mail": "user@myname.example.com"}},
+        }
+
+        filtered_results = general_filter.filter_results([result])
+        assert len(filtered_results) == 1
+        assert filtered_results[0]["properties"]["filtered"]["state"] == "included"
+        assert filtered_results[0]["properties"]["filtered"]["matchedFilter"] == [rule]
+        assert "warnings" not in filtered_results[0]["properties"]["filtered"]
+
+    def test_filter_results_regex_guid(self):
+        general_filter = GeneralFilter()
+        guid_rule = {
+            "properties.blame.author-mail": "/[0-9A-F]{8}[-][0-9A-F]{4}[-][0-9A-F]{4}"
+            + "[-][0-9A-F]{4}[-][0-9A-F]{12}/"
+        }
+        general_filter.init_filter(
+            "test filter",
+            {},
+            [guid_rule],
+            [],
+        )
+        result = {
+            "ruleId": "test-rule",
+            "properties": {
+                "blame": {"author-mail": "AAAAA1234ABCD-FEDC-BA09-8765-4321ABCDEF90"}
+            },
+        }
+
+        filtered_results = general_filter.filter_results([result])
+        assert len(filtered_results) == 1
+        assert filtered_results[0]["properties"]["filtered"]["state"] == "included"
+        assert filtered_results[0]["properties"]["filtered"]["matchedFilter"] == [
+            guid_rule
+        ]
+        assert "warnings" not in filtered_results[0]["properties"]["filtered"]
+
+    def test_filter_results_existence_only(self):
+        general_filter = GeneralFilter()
+        general_filter.init_filter("test filter", {}, [], [{"suppression": {}}])
+        result = {"ruleId": "test-rule", "suppressions": [{"kind": "inSource"}]}
+
+        filtered_results = general_filter.filter_results([result])
+        assert len(filtered_results) == 0
+
+    def test_filter_results_match_default_include_default_configuration(self):
+        general_filter = GeneralFilter()
+        general_filter.init_filter("test filter", {}, [{"level": "error"}], [])
+        result = {"ruleId": "test-rule"}
+
+        filtered_results = general_filter.filter_results([result])
+        assert len(filtered_results) == 1
+        assert filtered_results[0] == result
+        assert filtered_results[0]["properties"]["filtered"]["state"] == "default"
+        assert filtered_results[0]["properties"]["filtered"]["warnings"] == [
+            "Field 'level' is missing but the result included as default-include is true"
+        ]
+        assert general_filter.filter_stats.filtered_in_result_count == 0
+        assert general_filter.filter_stats.filtered_out_result_count == 0
+        assert general_filter.filter_stats.missing_property_count == 1
+
+    def test_filter_results_match_default_include_rule_override(self):
+        general_filter = GeneralFilter()
+        general_filter.init_filter(
+            "test filter",
+            {},
+            [{"level": {"value": "error", "default-include": False}}],
+            [],
+        )
+        result = {"ruleId": "test-rule"}
+
+        filtered_results = general_filter.filter_results([result])
+        assert len(filtered_results) == 0
+        assert general_filter.filter_stats.filtered_in_result_count == 0
+        assert general_filter.filter_stats.filtered_out_result_count == 0
+        assert general_filter.filter_stats.missing_property_count == 0
+
+    SHORTCUTS_TEST_PARAMS = [
+        ({"author": "John Smith"}, {"properties": {"blame": {"author": "John Smith"}}}),
+        (
+            {"author-mail": "john.smith@example.com"},
+            {"properties": {"blame": {"author-mail": "john.smith@example.com"}}},
+        ),
+        (
+            {"committer-mail": "john.smith@example.com"},
+            {"properties": {"blame": {"committer-mail": "john.smith@example.com"}}},
+        ),
+        (
+            {"location": "test.cpp"},
+            {
+                "locations": [
+                    {"physicalLocation": {"artifactLocation": {"uri": "test.cpp"}}}
+                ]
+            },
+        ),
+        ({"rule": "rule1"}, {"ruleId": "rule1"}),
+        ({"suppression": "inSource"}, {"suppressions": [{"kind": "inSource"}]}),
+    ]
+
+    @pytest.mark.parametrize("shortcut_filter,result", SHORTCUTS_TEST_PARAMS)
+    def test_filter_results_shortcuts(self, shortcut_filter, result):
+        general_filter = GeneralFilter()
+        general_filter.init_filter("test filter", {}, [shortcut_filter], [])
+
+        filtered_results = general_filter.filter_results([result])
+        assert len(filtered_results) == 1
+        assert filtered_results[0] == result
+        assert filtered_results[0]["properties"]["filtered"]["state"] == "included"
+        assert "warnings" not in filtered_results[0]["properties"]["filtered"]
+
+    def test_filter_results_include(self):
+        general_filter = GeneralFilter()
+        general_filter.init_filter("test filter", {}, [{"ruleId": "test-rule"}], [])
+        results = [{"ruleId": "test-rule"}] * 10
+
+        filtered_results = general_filter.filter_results(results)
+        assert len(filtered_results) == 10
+        assert all(result in filtered_results for result in results)
+        assert general_filter.filter_stats.filtered_in_result_count == 10
+        assert general_filter.filter_stats.filtered_out_result_count == 0
+        assert general_filter.filter_stats.missing_property_count == 0
+
+    def test_filter_results_exclude(self):
+        general_filter = GeneralFilter()
+        general_filter.init_filter("test filter", {}, [], [{"level": "error"}])
+        results = [{"level": "error"}] * 10
+
+        filtered_results = general_filter.filter_results(results)
+        assert len(filtered_results) == 0
+        assert general_filter.filter_stats.filtered_in_result_count == 0
+        assert general_filter.filter_stats.filtered_out_result_count == 10
+        assert general_filter.filter_stats.missing_property_count == 0
+
+    def test_filter_results_no_filters(self):
+        general_filter = GeneralFilter()
+        general_filter.init_filter("test filter", {}, [], [])
+        results = [{"ruleId": "test-rule"}] * 10
+
+        filtered_results = general_filter.filter_results(results)
+        assert len(filtered_results) == 10
+        assert all(result in filtered_results for result in results)
+        assert general_filter.filter_stats.filtered_in_result_count == 0
+        assert general_filter.filter_stats.filtered_out_result_count == 0
+        assert general_filter.filter_stats.missing_property_count == 0
+
+    def test_get_filter_stats(self):
+        general_filter = GeneralFilter()
+        general_filter.init_filter("test filter", {}, [{"ruleId": "test-rule"}], [])
+        results = [{"ruleId": "test-rule"}] * 10
+
+        general_filter.filter_results(results)
+        filter_stats = general_filter.get_filter_stats()
+        assert filter_stats.filtered_in_result_count == 10
+        assert filter_stats.filtered_out_result_count == 0
+        assert filter_stats.missing_property_count == 0
+
+    def test_load_filter_file(self):
+        file_path = "test_filter.yaml"
+        filter_description = "Test filter"
+        include_filters = {"ruleId": "test-rule"}
+        exclude_filters = {"level": "error"}
+        with open(file_path, "w") as f:
+            f.write(f"description: {filter_description}\n")
+            f.write(f"include:\n  ruleId: {include_filters['ruleId']}\n")
+            f.write(f"exclude:\n  level: {exclude_filters['level']}\n")
+
+        loaded_filter = load_filter_file(file_path)
+        assert loaded_filter == (
+            filter_description,
+            {},
+            include_filters,
+            exclude_filters,
+        )
+
+    def test_load_filter_file_with_configuration(self):
+        file_path = "test_filter.yaml"
+        filter_description = "Test filter"
+        configuration = {"default-include": True}
+        include_filters = {"ruleId": "test-rule"}
+        exclude_filters = {"level": "error"}
+        with open(file_path, "w") as f:
+            f.write(f"description: {filter_description}\n")
+            f.write("configuration:\n  default-include: true\n")
+            f.write(f"include:\n  ruleId: {include_filters['ruleId']}\n")
+            f.write(f"exclude:\n  level: {exclude_filters['level']}\n")
+
+        loaded_filter = load_filter_file(file_path)
+        assert loaded_filter == (
+            filter_description,
+            configuration,
+            include_filters,
+            exclude_filters,
+        )
+
+    def test_load_filter_file_wrong_format(self):
+        file_path = "test_filter.yaml"
+        filter_description = "Test filter"
+        with open(file_path, "w") as f:
+            f.write(f"description: {filter_description}\n")
+            f.write("include\n")
+            f.write("exclude\n")
+
+        with pytest.raises(IOError) as io_error:
+            load_filter_file(file_path)
+        assert str(io_error.value) == f"Cannot read filter file {file_path}"