Repository: microsoft/msticpy (language: python)

Issue:
Updated Cybereason docs to fit pattern
Updated Cybereason provider docs to fit new documentation pattern around Data Providers.
Ianhelle/1.6.1 hotfixes 2022 01 27 for 1.6.2
Hotfixes for keyvault authentication and mordor browser
Fix for IPython (py-specific specifier) to allow MSTICPy to install in Py 3.6
Fixes for test cases to use proper pkg_resources Requirement parsers
Updated Kqlmagic custom version to release version
Bump respx from 0.17.1 to 0.19.2
Bumps [respx](https://github.com/lundberg/respx) from 0.17.1 to 0.19.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/lundberg/respx/releases">respx's releases</a>.</em></p>
<blockquote>
<h2>Version 0.19.2</h2>
<h2>0.19.2 (3rd February 2022)</h2>
<h3>Fixed</h3>
<ul>
<li>Better cleanup before building egg, thanks <a href="https://github.com/nebularazer"><code>@​nebularazer</code></a> (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/198">#198</a>)</li>
</ul>
<h2>Version 0.19.1</h2>
<h2>0.19.1 (10th Januari 2022)</h2>
<h3>Fixed</h3>
<ul>
<li>Allow first path segments containing colons, thanks <a href="https://github.com/hannseman"><code>@​hannseman</code></a>. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/192">#192</a>)</li>
<li>Fix license classifier, thanks <a href="https://github.com/shadchin"><code>@​shadchin</code></a> (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/195">#195</a>)</li>
<li>Fix typos, thanks <a href="https://github.com/kianmeng"><code>@​kianmeng</code></a> (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/194">#194</a>)</li>
</ul>
<h2>Version 0.19.0</h2>
<h2>0.19.0 (15th November 2021)</h2>
<h3>Fixed</h3>
<ul>
<li>Support HTTPX 0.21.0. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/189">#189</a>)</li>
<li>Use Session.notify when chaining nox sessions, thanks <a href="https://github.com/flaeppe"><code>@​flaeppe</code></a>. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/188">#188</a>)</li>
<li>Add overloads to MockRouter.<strong>call</strong>, thanks <a href="https://github.com/flaeppe"><code>@​flaeppe</code></a>. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/187">#187</a>)</li>
<li>Enhance AND pattern evaluation to fail fast. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/185">#185</a>)</li>
<li>Fix CallList assertion error message. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/178">#178</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Prevent method and url as lookups in HTTP method helpers, thanks <a href="https://github.com/flaeppe"><code>@​flaeppe</code></a>. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/183">#183</a>)</li>
<li>Fail pattern match when JSON path not found. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/184">#184</a>)</li>
</ul>
<h2>Version 0.18.2</h2>
<h2>0.18.2 (22nd October 2021)</h2>
<h3>Fixed</h3>
<ul>
<li>Include extensions when instantiating request in HTTPCoreMocker (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/176">#176</a>)</li>
</ul>
<h2>Version 0.18.1</h2>
<h2>0.18.1 (20th October 2021)</h2>
<h3>Fixed</h3>
<ul>
<li>Respect ordered param values. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/172">#172</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Raise custom error types for assertion checks. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/174">#174</a>)</li>
</ul>
<h2>Version 0.18.0</h2>
<h2>0.18.0 (14th October 2021)</h2>
<h3>Fixed</h3>
<ul>
<li>Downgrade <code>HTTPX</code> requirement to 0.20.0. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/170">#170</a>)</li>
</ul>
<h3>Added</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/lundberg/respx/blob/master/CHANGELOG.md">respx's changelog</a>.</em></p>
<blockquote>
<h2>[0.19.2] - 2022-02-03</h2>
<h3>Fixed</h3>
<ul>
<li>Better cleanup before building egg, thanks <a href="https://github.com/nebularazer"><code>@​nebularazer</code></a> (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/198">#198</a>)</li>
</ul>
<h2>[0.19.1] - 2022-01-10</h2>
<h3>Fixed</h3>
<ul>
<li>Allow first path segments containing colons, thanks <a href="https://github.com/hannseman"><code>@​hannseman</code></a>. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/192">#192</a>)</li>
<li>Fix license classifier, thanks <a href="https://github.com/shadchin"><code>@​shadchin</code></a> (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/195">#195</a>)</li>
<li>Fix typos, thanks <a href="https://github.com/kianmeng"><code>@​kianmeng</code></a> (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/194">#194</a>)</li>
</ul>
<h2>[0.19.0] - 2021-11-15</h2>
<h3>Fixed</h3>
<ul>
<li>Support HTTPX 0.21.0. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/189">#189</a>)</li>
<li>Use Session.notify when chaining nox sessions, thanks <a href="https://github.com/flaeppe"><code>@​flaeppe</code></a>. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/188">#188</a>)</li>
<li>Add overloads to MockRouter.<strong>call</strong>, thanks <a href="https://github.com/flaeppe"><code>@​flaeppe</code></a>. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/187">#187</a>)</li>
<li>Enhance AND pattern evaluation to fail fast. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/185">#185</a>)</li>
<li>Fix CallList assertion error message. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/178">#178</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Prevent method and url as lookups in HTTP method helpers, thanks <a href="https://github.com/flaeppe"><code>@​flaeppe</code></a>. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/183">#183</a>)</li>
<li>Fail pattern match when JSON path not found. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/184">#184</a>)</li>
</ul>
<h2>[0.18.2] - 2021-10-22</h2>
<h3>Fixed</h3>
<ul>
<li>Include extensions when instantiating request in HTTPCoreMocker. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/176">#176</a>)</li>
</ul>
<h2>[0.18.1] - 2021-10-20</h2>
<h3>Fixed</h3>
<ul>
<li>Respect ordered param values. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/172">#172</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Raise custom error types for assertion checks. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/174">#174</a>)</li>
</ul>
<h2>[0.18.0] - 2021-10-14</h2>
<h3>Fixed</h3>
<ul>
<li>Downgrade <code>HTTPX</code> requirement to 0.20.0. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/170">#170</a>)</li>
</ul>
<h3>Added</h3>
<ul>
<li>Add support for matching param with <em>ANY</em> value. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/167">#167</a>)</li>
</ul>
<h2>[0.18.0b0] - 2021-09-15</h2>
<h3>Changed</h3>
<ul>
<li>Deprecate RESPX MockTransport in favour of HTTPX MockTransport. (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/152">#152</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/lundberg/respx/commit/c76f632690cdfb2c878fe8fea999df0a26083eb3"><code>c76f632</code></a> Release 0.19.2</li>
<li><a href="https://github.com/lundberg/respx/commit/e619c8547b273e0ad6f3d4d863a783a1ba764eae"><code>e619c85</code></a> Drop old build dir before building egg</li>
<li><a href="https://github.com/lundberg/respx/commit/61b749c5ce2c8a64adf4769a5160a5cca41c25a8"><code>61b749c</code></a> Release 0.19.1</li>
<li><a href="https://github.com/lundberg/respx/commit/f7e10b1f7c71b4af06bb829613dbca61102c954b"><code>f7e10b1</code></a> Fix typos (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/194">#194</a>)</li>
<li><a href="https://github.com/lundberg/respx/commit/1cdeb19773a1a99f7b6cd41b259efcc778f96a97"><code>1cdeb19</code></a> Fix license classifier (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/195">#195</a>)</li>
<li><a href="https://github.com/lundberg/respx/commit/0c94bae69a1fd3ac7512b81daf32ef99db08cf8d"><code>0c94bae</code></a> Allow first path segments containing colons (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/192">#192</a>)</li>
<li><a href="https://github.com/lundberg/respx/commit/3981b5b4e3301bb6099bed8b45ecbdd8c5aec89f"><code>3981b5b</code></a> Release 0.19.0</li>
<li><a href="https://github.com/lundberg/respx/commit/6b2454658d1753d5bfbe6d9bd0cffb48a26e8453"><code>6b24546</code></a> Support HTTPX 0.21.0 (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/189">#189</a>)</li>
<li><a href="https://github.com/lundberg/respx/commit/b5953e25be6bc7b22efcf7c921df7aac0d2ef1d9"><code>b5953e2</code></a> Use Session.notify when chaining nox sessions (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/188">#188</a>)</li>
<li><a href="https://github.com/lundberg/respx/commit/8cc098fa1ad0104da7c73daea2075c3714000cee"><code>8cc098f</code></a> Add overloads to MockRouter.<strong>call</strong> (<a href="https://github-redirect.dependabot.com/lundberg/respx/issues/187">#187</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/lundberg/respx/compare/0.17.1...0.19.2">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=respx&package-manager=pip&previous-version=0.17.1&new-version=0.19.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Discussion / hints:

Check out this pull request on&nbsp; <a href="https://app.reviewnb.com/microsoft/msticpy/pull/317"><img align="absmiddle"  alt="ReviewNB" height="28" class="BotMessageButtonImage" src="https://raw.githubusercontent.com/ReviewNB/support/master/images/button_reviewnb.png"/></a> 

 See visual diffs & provide feedback on Jupyter Notebooks. 

---

 <i>Powered by <a href='https://www.reviewnb.com/?utm_source=gh'>ReviewNB</a></i>


Fix patch:
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 000000000..c4ae7b355
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,36 @@
+﻿# See here for image contents: https://hub.docker.com/r/jupyter/scipy-notebook
+FROM jupyter/scipy-notebook
+LABEL maintainer="Msticpy Dev Team <msticpy@microsoft.com>"
+
+#installing Msticpy requirements and dependencies
+COPY ./requirements-all.txt /tmp/
+COPY ./requirements-dev.txt /tmp/
+RUN pip install --requirement /tmp/requirements-all.txt & \
+pip install --requirement /tmp/requirements-dev.txt & \
+pip install -e git+https://github.com/microsoft/msticpy
+COPY . /tmp/
+
+# Activate ipywidgets extension in the environment that runs the notebook server
+RUN jupyter nbextension enable --py widgetsnbextension --sys-prefix && \
+    jupyter nbextension enable toc2/main --sys-prefix && \
+    jupyter nbextension enable execute_time/ExecuteTime --sys-prefix && \
+    jupyter nbextension enable python-markdown/main --sys-prefix && \
+    jupyter nbextension enable codefolding/main --sys-prefix && \
+    jupyter nbextension enable autosavetime/main --sys-prefix && \
+    jupyter nbextension enable tree-filter/index --sys-prefix && \
+    jupyter nbextension enable hide_input_all/main --sys-prefix && \
+    jupyter nbextension enable hinterland/hinterland --sys-prefix && \
+    jupyter nbextension enable varInspector/main --sys-prefix && \
+    jupyter nbextension enable spellchecker/main --sys-prefix && \
+    jupyter nbextension enable toggle_all_line_numbers/main --sys-prefix && \
+    jupyter nbextension enable --py qgrid --sys-prefix && \
+    jupyter nbextension install https://github.com/drillan/jupyter-black/archive/master.zip --sys-prefix && \
+    jupyter nbextension install --py nbdime --sys-prefix && \
+    jupyter nbextension enable jupyter-black-master/jupyter-black --sys-prefix && \
+    jupyter nbextension enable nbdime --py --sys-prefix && \
+    jupyter nbextension enable rise --py --sys-prefix && \
+    jupyter nbextensions_configurator enable --sys-prefix && \
+    jupyter labextension install @jupyterlab/git --no-build && \
+    jupyter labextension install @jupyterlab/github --no-build && \
+    jupyter labextension enable git && \
+    jupyter lab build --dev-build=False
\ No newline at end of file
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..a8677313b
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,78 @@
+﻿	{
+	"name": "Msticpy Python Development Container",
+	"build": {
+		"context": "..",
+		"dockerfile": "Dockerfile"
+	},
+
+	// Set *default* container specific settings.json values on container create.
+	"settings": {
+		"python.defaultInterpreterPath": "/opt/conda/bin/python",
+		"python.linting.enabled": true,
+		"python.linting.flake8Path": "/opt/conda/bin/flake8",
+		"python.formatting.provider": "black",
+		"python.linting.pycodestylePath": "/opt/conda/bin/pycodestyle",
+		"python.linting.pydocstylePath": "/opt/conda/bin/pydocstyle",
+		"python.linting.pylintPath": "/opt/conda/bin/pylint",
+		"files.autoSave": "afterDelay",
+		"team.showWelcomeMessage": false,
+		"python.linting.pylintEnabled": true,
+		"python.linting.pydocstyleArgs": [""],
+		"python.linting.pep8Enabled": true,
+		"python.linting.pep8Args": ["--ignore=E501,E251,W503,E701"],
+		"python.linting.flake8Enabled": true,
+		"python.linting.flake8Args": ["--max-line-length=90"],
+		"python.linting.pydocstyleEnabled": true,
+		"python.linting.mypyEnabled": true,
+		"python.linting.pylintUseMinimalCheckers": false,
+		"python.linting.prospectorEnabled": true,
+		"python.linting.banditEnabled": true,
+		"python.linting.ignorePatterns": [
+		".vscode/*.py",
+		"**/site-packages/**/*.py",
+		"**/tests/test_*.py",
+		"docs/**",
+		"docs/source/**",
+		"docs/build/**"
+		],
+		"editor.tabSize": 4,
+		"editor.suggestSelection": "first",
+		"vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue",
+		"autoDocstring.docstringFormat": "numpy",
+		"git.autofetch": true,
+		"python.terminal.activateEnvironment": false,
+		"restructuredtext.linter.extraArgs": [
+		"--ignore D002",
+		"--ignore D004"
+		],
+		"[python]": {
+		},
+		"python.testing.pyTestArgs": [
+		"tests"
+		],
+		"python.testing.unittestEnabled": false,
+		"python.testing.nosetestsEnabled": false,
+		"python.testing.pyTestEnabled": true,
+	},
+
+	// Add the IDs of extensions you want installed when the container is created.
+	"extensions": [
+		"eamodio.gitlens",
+		"lextudio.restructuredtext",
+		"mohsen1.prettify-json",
+		"ms-python.python",
+		"ms-python.vscode-pylance",
+		"njpwerner.autodocstring",
+		"vscode-icons-team.vscode-icons",
+		"github.vscode-pull-request-github"
+	],
+
+	// Use 'forwardPorts' to make a list of ports inside the container available locally.
+	// "forwardPorts": [],
+
+	// Use 'postCreateCommand' to run commands after the container is created.
+	// "postCreateCommand": "python --version",
+
+	// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
+	"remoteUser": "vscode"
+}
\ No newline at end of file
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index 41fde8a11..58fb81180 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -79,7 +79,7 @@ jobs:
       if: ${{ always() }}
     - name: black
       run: |
-        black -t py36 --diff --check --exclude venv .
+        black --diff --check --exclude venv .
       if: ${{ always() }}
     - name: flake8
       run: |
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index dc5ab32ec..674082f90 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,6 +4,7 @@ repos:
     hooks:
       - id: check-yaml
       - id: check-json
+        exclude: .*devcontainer.json
       - id: trailing-whitespace
         args: [--markdown-linebreak-ext=md]
   - repo: https://github.com/ambv/black
@@ -11,9 +12,6 @@ repos:
     hooks:
       - id: black
         language: python
-        args:
-          - -t
-          - py36
   - repo: https://github.com/PyCQA/pylint
     rev: v2.12.2
     hooks:
@@ -29,14 +27,14 @@ repos:
           - --extend-ignore=E0401,E501
           - --max-line-length=90
           - --exclude=tests,test*.py
-  # - repo: https://github.com/pycqa/isort
-  #   rev: 5.10.1
-  #   hooks:
-  #     - id: isort
-  #       name: isort (python)
-  #       args:
-  #         - --profile
-  #         - black
+  - repo: https://github.com/pycqa/isort
+    rev: 5.10.1
+    hooks:
+      - id: isort
+        name: isort (python)
+        args:
+          - --profile
+          - black
   - repo: local
     hooks:
       - id: check_reqs_all
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index b0cacd812..d9824ee63 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -165,7 +165,7 @@ stages:
         condition: succeededOrFailed()
       - script: |
           python -m pip install --upgrade black
-          black -t py36 --diff --check --exclude venv .
+          black --diff --check --exclude venv .
         displayName: Black
         continueOnError: true
         condition: succeededOrFailed()
diff --git a/conda/conda-reqs-dev.txt b/conda/conda-reqs-dev.txt
index 5aa989f01..1bf1f6f7b 100644
--- a/conda/conda-reqs-dev.txt
+++ b/conda/conda-reqs-dev.txt
@@ -19,7 +19,7 @@ pyflakes>=2.2.0
 pylint>=2.5.3
 pytest-cov>=2.11.1
 pytest>=5.0.1
-respx>=0.19.0
+respx==0.19.2
 sphinx_rtd_theme>=0.5.1
 sphinx>=2.1.2
 virtualenv
diff --git a/conda/conda-reqs-pip.txt b/conda/conda-reqs-pip.txt
index 679a36ec4..ad169520e 100644
--- a/conda/conda-reqs-pip.txt
+++ b/conda/conda-reqs-pip.txt
@@ -11,7 +11,7 @@ azure-storage-blob>=12.5.0
 geoip2>=2.9.0
 html5lib
 ipwhois>=1.1.0
-KqlmagicCustom[jupyter-basic,auth_code_clipboard]>=0.1.114.dev26
+KqlmagicCustom[jupyter-basic,auth_code_clipboard]>=0.1.114
 moz_sql_parser>=4.5.0,<=4.11.21016
 passivetotal>=2.5.3
 splunk-sdk>=1.6.0
diff --git a/docs/notebooks/AzureSentinelAPIs.ipynb b/docs/notebooks/AzureSentinelAPIs.ipynb
index 0f71a01d0..d7fe74b79 100644
--- a/docs/notebooks/AzureSentinelAPIs.ipynb
+++ b/docs/notebooks/AzureSentinelAPIs.ipynb
@@ -45,7 +45,7 @@
    },
    "outputs": [],
    "source": [
-    "from msticpy.data.azure_sentinel import AzureSentinel\n",
+    "from msticpy.data.azure.sentinel_core import MicrosoftSentinel\n",
     "import msticpy.nbtools.nbwidgets as widgets\n",
     "from msticpy.data import data_obfus as mask"
    ]
@@ -77,7 +77,7 @@
     }
    ],
    "source": [
-    "azs = AzureSentinel()\n",
+    "azs = MicrosoftSentinel()\n",
     "azs.connect()"
    ]
   },
@@ -1807,7 +1807,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.7.9"
+   "version": "3.7.6"
   },
   "latex_envs": {
    "LaTeX_envs_menu_present": true,
diff --git a/docs/notebooks/VirusTotalLookup.ipynb b/docs/notebooks/VirusTotalLookup.ipynb
index d3e9d3f96..d7b582324 100644
--- a/docs/notebooks/VirusTotalLookup.ipynb
+++ b/docs/notebooks/VirusTotalLookup.ipynb
@@ -6,9 +6,15 @@
    "source": [
     "# Title: msticpy - VirusTotal Lookup\n",
     "\n",
+    "## Note that the original VT Lookup based on the V2 API is no longer supported\n",
+    "It will continue to work as long as VirusTotal supports it.\n",
+    "However, we may remove this functionality when replaced with an equivalent\n",
+    "\n",
+    "We recommend using the TILookup library and the VTLookupV3 library as alternatives.\n",
     "## Disclaimer and Acknowledgements:\n",
+    "\n",
     "The code in this module is offered as a convenience wrapper for the VirusTotal API based on the [public documentation](https://www.virustotal.com/en/documentation/public-api/). The code does not originate from VirusTotal, nor is it endorsed by them. I'd like thank them for\n",
-    "- Wonderfully clear documention and examples\n",
+    "- Wonderfully clear documentation and examples\n",
     "- Granting me extra querying capacity for my account for testing\n",
     "\n",
     "You must have msticpy installed to run this notebook:\n",
diff --git a/docs/source/DataAcquisition.rst b/docs/source/DataAcquisition.rst
index 2e2769fbe..565327e6b 100644
--- a/docs/source/DataAcquisition.rst
+++ b/docs/source/DataAcquisition.rst
@@ -25,6 +25,7 @@ Individual Data Environments
    data_acquisition/MordorData
    data_acquisition/DataProv-Sumologic
    data_acquisition/DataProv-Kusto
+   data_acquisition/DataProv-Cybereason
 
 
 Built-in Data Queries
diff --git a/docs/source/data_acquisition/CybereasonProvider.rst b/docs/source/data_acquisition/DataProv-Cybereason.rst
similarity index 90%
rename from docs/source/data_acquisition/CybereasonProvider.rst
rename to docs/source/data_acquisition/DataProv-Cybereason.rst
index c49d64b73..0a70c1d18 100644
--- a/docs/source/data_acquisition/CybereasonProvider.rst
+++ b/docs/source/data_acquisition/DataProv-Cybereason.rst
@@ -1,25 +1,14 @@
-Splunk - Data Connector
-=======================
+Cybereason Provider
+===================
 
-Description
------------
+This driver provides functions to allow you to query a Cybereason instance.
 
-The data provider module of msticpy provides functions to allow for the
-defining of data sources, connectors to them and queries for them as
-well as the ability to return query result from the defined data
-sources.
-
-For more information on Data Providers, check the documentation :doc:`DataProviders`
-
-In this notebooks we will demonstrate Cybereason data connector feature of
-msticpy.
-
-Authentication
-~~~~~~~~~~~~~~
+Cybereason Configuration
+------------------------
 
 Authentication for the Cybereason data provider is handled by specifying
 credentials directly in the connect call or specifying the credentials
-in msticpy config file.
+in MSTICPy config file.
 
 For more information on how to create new user with approapriate roles
 and permissions, follow the product documentation:
@@ -27,6 +16,9 @@ and permissions, follow the product documentation:
 and
 `API Guide <https://nest.cybereason.com/documentation/api-documentation/all-versions/cybereason-api-guide>`__.
 
+Cybereason Configuration in MSTICPy
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 Once you created user account with the appropriate roles, you will
 require the following details to specify while connecting:
 - TenantId = "instance". As this is a cloud-based solution, each customer has its dedicated instance. FQDN will be formatted as: "<instance>.cybereason.net"
@@ -44,12 +36,12 @@ shown in below example
       ClientId: account
       ClientSecret: yoursecret
 
-Instantiating a query provider
-------------------------------
+Loading and Connecting a QueryProvider for Cybereason
+-----------------------------------------------------
 
 You can instantiate a data provider for Cybereason by specifying the
-credentials in connect or in msticpy config file. If the details are
-correct and authentication is successful, it will show connected.
+credentials in connect or if left blank details from your MSTICPy config file will be used.
+If the details are correct and authentication is successful, it will show connected.
 
 .. code:: ipython3
 
@@ -85,11 +77,11 @@ as list datatypes, list saved searches, alerts, audittrail informaion.
 
 .. parsed-literal::
 
-    ['Connection.list_connections_from_process',
-    'Host.find_hosts',
-    'Process.find_process_by_commandLine',
-    'Process.find_process_by_pid',
-    'Process.find_process_by_suspicions']
+  ['Connection.list_connections_from_process',
+  'Host.find_hosts',
+  'Process.find_process_by_commandLine',
+  'Process.find_process_by_pid',
+  'Process.find_process_by_suspicions']
 
 
 
@@ -107,7 +99,7 @@ For more information, see
 .. parsed-literal::
 
     Query:  list_connections_from_process
-    Data source:  Splunk
+    Data source:  Cybereason
     Search for process with a specific suspicion
 
     Parameters
@@ -251,8 +243,9 @@ use below example
 Running pre-defined queries
 ---------------------------
 
-In order to run pre-defined query , execute with the name either by
-setting values for arguments if available or run with default arguments.
+In order to run pre-defined query, call the provider followed by the query name.
+Pre-defined queries can be run with either values specified as arguments
+or run with default arguments.
 
 For more information , refer to the documentation
 :ref:`Running a pre-defined query <data_acquisition/dataproviders:running a pre-defined query>`
@@ -334,10 +327,10 @@ For more information , refer to the documentation
 
 
 
-Running an ad hoc Splunk query
-------------------------------
+Running an ad-hoc Cybereason query
+----------------------------------
 
-You can also create your own query and run it via the Splunk
+You can also create your own query and run it via the Cybereason
 provider using this syntax:
 ``QUERY_PROVIDER.exec_query(<query_text>)``
 
@@ -425,8 +418,8 @@ For more information, check documentation :ref:`data_acquisition/dataproviders:r
 
 |
 
-References
-----------
+Other Cybereason Documentation
+------------------------------
 
 -  `Cybereason Documentation
    <https://nest.cybereason.com/documentation/product-documentation/202/cybereason-202-documentation>`__
diff --git a/docs/source/data_acquisition/DataProv-MSSentinel.rst b/docs/source/data_acquisition/DataProv-MSSentinel.rst
index e470117f9..e6e7e6c80 100644
--- a/docs/source/data_acquisition/DataProv-MSSentinel.rst
+++ b/docs/source/data_acquisition/DataProv-MSSentinel.rst
@@ -5,7 +5,7 @@ Sentinel Configuration
 ----------------------
 
 You can store configuration for your workspace (or workspaces) in either
-your ``msticpconfig.yaml`` or a ``config.json`` file. The latter
+your ``msticpyconfig.yaml`` or a ``config.json`` file. The latter
 file is auto-created in your Azure Machine Learning (AML) workspace when
 you launch a notebook from the Sentinel portal. It can however, only
 store details for a single workspace.
@@ -42,20 +42,20 @@ The MS Sentinel connection settings are stored in the
           TenantId: 335b56ab-67a2-4118-ac14-6eb454f350af
           ResourceGroup: soc
           SubscriptionId: a5b24e23-a96a-4472-b729-9e5310c83e20
-          Workspace Name: Workspace1
+          WorkspaceName: Workspace1
         # To use these launch with an explicit name - WorkspaceConfig(workspace_name="Workspace2")
         Workspace1:
           WorkspaceId: "c88dd3c2-d657-4eb3-b913-58d58d811a41"
           TenantId: "335b56ab-67a2-4118-ac14-6eb454f350af"
           ResourceGroup: soc
           SubscriptionId: a5b24e23-a96a-4472-b729-9e5310c83e20
-          Workspace Name: Workspace1
+          WorkspaceName: Workspace1
         TestWorkspace:
           WorkspaceId: "17e64332-19c9-472e-afd7-3629f299300c"
           TenantId: "4ea41beb-4546-4fba-890b-55553ce6003a"
           ResourceGroup: soc
           SubscriptionId: a5b24e23-a96a-4472-b729-9e5310c83e20
-          Workspace Name: Workspace2
+          WorkspaceName: Workspace2
 
 If you only use a single workspace, you only need to create a ``Default`` entry and
 add the values for your *WorkspaceID* and *TenantID*. You can add other entries here,
diff --git a/docs/source/data_acquisition/DataProviders.rst b/docs/source/data_acquisition/DataProviders.rst
index e297fdde8..8f544d40b 100644
--- a/docs/source/data_acquisition/DataProviders.rst
+++ b/docs/source/data_acquisition/DataProviders.rst
@@ -46,7 +46,8 @@ the available options.
     'Mordor',
     'ResourceGraph',
     'Sumologic',
-    'M365D']
+    'M365D',
+    'Cybereason']
 
 .. note:: New providers are being added regularly so this list
     may look a little different. Also some items in this list
diff --git a/msticpy/analysis/__init__.py b/msticpy/analysis/__init__.py
index a89785781..b14a3ad5f 100644
--- a/msticpy/analysis/__init__.py
+++ b/msticpy/analysis/__init__.py
@@ -22,10 +22,4 @@
 # pylint: disable=W0401
 from ..data.context.geoip import GeoLiteLookup, IPStackLookup, geo_distance
 
-try:
-    from IPython import get_ipython
-    from . import sectools_magics
-except ImportError as err:
-    pass
-
 __version__ = VERSION
diff --git a/msticpy/analysis/sectools_magics.py b/msticpy/analysis/sectools_magics.py
deleted file mode 100644
index 4206e7d92..000000000
--- a/msticpy/analysis/sectools_magics.py
+++ /dev/null
@@ -1,155 +0,0 @@
-# -------------------------------------------------------------------------
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License. See License.txt in the project root for
-# license information.
-# --------------------------------------------------------------------------
-"""msticpy IPython magics."""
-import re
-from typing import Tuple, List
-
-# pylint: disable=unused-import
-# flake8: noqa: F403
-import pandas as pd
-
-# pylint: enable=unused-import
-from IPython import get_ipython
-from IPython.core import magic_arguments
-from IPython.core.magic import line_cell_magic, Magics, magics_class
-
-try:
-    from bs4 import BeautifulSoup
-
-    _BS_AVAILABLE = True
-except ImportError:
-    _BS_AVAILABLE = False
-
-from .data import base64unpack as base64
-from .data.iocextract import IoCExtract
-
-from .._version import VERSION
-
-__version__ = VERSION
-__author__ = "Ian Hellen"
-
-
-@magics_class
-class Base64Magic(Magics):
-    """Base64 IPython magic extension."""
-
-    _STRIP_TAGS = r"</?decoded[^>]*>"
-
-    @line_cell_magic
-    @magic_arguments.magic_arguments()
-    @magic_arguments.argument(
-        "--out", "-o", help="The variable to return the results in"
-    )
-    @magic_arguments.argument(
-        "--pretty", "-p", help="Print formatted version of output", action="store_true"
-    )
-    @magic_arguments.argument(
-        "--clean",
-        "-c",
-        help="Print decoded string with no formatting",
-        action="store_true",
-    )
-    def b64(self, line: str = "", cell: str = None) -> str:
-        """
-        Base64 IPython magic extension.
-
-        Parameters
-        ----------
-        line : str, optional
-            Line contents, by default ""
-        cell : str, optional
-            Cell contents, by default None
-
-        Returns
-        -------
-        str
-            Decoded text
-
-        """
-        if cell is None:
-            results, df_results = base64.unpack(line)
-
-        else:
-            results, df_results = base64.unpack(cell)
-        args = magic_arguments.parse_argstring(self.b64, line)
-
-        if args.clean:
-            results = re.sub(self._STRIP_TAGS, "", results)
-        elif args.pretty:
-            if _BS_AVAILABLE:
-                xml_str = f"<decoded_string>{results}</decoded_string>"
-                b_soup = BeautifulSoup(xml_str, "xml")
-                results = b_soup.prettify()
-        if args.out is not None:
-            self.shell.user_ns[args.out] = (results, df_results)
-        return results
-
-
-@magics_class
-class IoCExtractMagic(Magics):
-    """Ioc Extract IPython magic extension."""
-
-    def __init__(self, shell):
-        """
-        Instantiate magic class.
-
-        Parameters
-        ----------
-        shell : IPython shell
-            IPython shell
-
-        """
-        # You must call the parent constructor
-        super().__init__(shell)
-        self._ioc_extract = IoCExtract()
-
-    @line_cell_magic
-    @magic_arguments.magic_arguments()
-    @magic_arguments.argument(
-        "--out", "-o", help="The variable to return the results in"
-    )
-    @magic_arguments.argument(
-        "--ioc_types",
-        "-i",
-        help="The types of IoC to search for (comma-separated string)",
-    )
-    def ioc(self, line="", cell=None) -> List[Tuple[str, List[str]]]:
-        """
-        Ioc Extract IPython magic extension.
-
-        Parameters
-        ----------
-        line : str, optional
-            Line contents, by default ""
-        cell : str, optional
-            Cell contents, by default None
-
-        Returns
-        -------
-        List[Tuple[str, List[str]]]
-            List of tuples of IoCs found grouped by type.
-
-        """
-        args = magic_arguments.parse_argstring(self.ioc, line)
-        ioc_types = None
-        if args.ioc_types:
-            ioc_types = [ioc_type.strip() for ioc_type in args.ioc_types.split(",")]
-
-        if cell is None:
-            results = self._ioc_extract.extract(src=line, ioc_types=ioc_types)
-        else:
-            results = self._ioc_extract.extract(src=cell, ioc_types=ioc_types)
-        iocs = [(ioc_type, list(ioc_res)) for ioc_type, ioc_res in results.items()]
-
-        if args.out is not None:
-            self.shell.user_ns[args.out] = results
-        return iocs
-
-
-IPYTHON = get_ipython()
-if IPYTHON:
-    IPYTHON.register_magics(Base64Magic)
-    IPYTHON.register_magics(IoCExtractMagic)
diff --git a/msticpy/common/exceptions.py b/msticpy/common/exceptions.py
index 90f03f24b..dc10bcf4b 100644
--- a/msticpy/common/exceptions.py
+++ b/msticpy/common/exceptions.py
@@ -65,13 +65,15 @@ def __init__(
 
         Other Parameters
         ----------------
-        title : str
+        title : str, optional
             If a `title` keyword argument is supplied it will be used
             to create the title line.
-        *_uri : str
+        *_uri : str, optional
             Additional keyword arguments who's names end in "_uri"
             will be used to create a list of references in addition to
             the primary `help_uri`
+        display : bool, optional
+            Display the exception when created. By default, True
 
         Notes
         -----
@@ -95,6 +97,7 @@ def __init__(
             Union[str, Tuple[str, str], Tuple[Tuple[str, str], str]]
         ] = []
         title = kwargs.pop("title", "we've hit an error while running")
+        disp_exception = kwargs.pop("display", True)
         self._output.append((f"{self.__class__.__name__} - {title}", "title"))
 
         self._output.extend(args)
@@ -111,7 +114,7 @@ def __init__(
             self._output.append("You can find other related help here:")
             for uri in help_args:
                 self._output.append((uri, "uri"))
-        if _get_config("msticpy.FriendlyExceptions"):
+        if _get_config("msticpy.FriendlyExceptions") and disp_exception:
             self._display_exception()
 
         # add the extra elements to the the exception standard args.
diff --git a/msticpy/common/keyvault_client.py b/msticpy/common/keyvault_client.py
index 64904c268..33dcfe337 100644
--- a/msticpy/common/keyvault_client.py
+++ b/msticpy/common/keyvault_client.py
@@ -8,7 +8,7 @@
 import json
 from typing import Any, List
 
-from azure.core.exceptions import ResourceNotFoundError
+from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
 from azure.keyvault.secrets import KeyVaultSecret, SecretClient
 
 from azure.mgmt.keyvault import KeyVaultManagementClient
@@ -24,17 +24,31 @@
     VaultProperties,
 )
 
+from IPython.display import display, HTML
 from msrestazure.azure_exceptions import CloudError
 
 from .._version import VERSION
 from .azure_auth_core import az_connect_core
-from .exceptions import MsticpyKeyVaultConfigError, MsticpyKeyVaultMissingSecretError
+from .exceptions import (
+    MsticpyKeyVaultConfigError,
+    MsticpyKeyVaultMissingSecretError,
+    MsticpyUserConfigError,
+)
 from .keyvault_settings import KeyVaultSettings
-from .utility import export
+from .utility import export, is_ipython
 
 __version__ = VERSION
 __author__ = "Matt Richard, Ian Hellen"
 
+_KV_CLIENT_AUTH_ERROR = [
+    "Please use Azure CLI authentication:",
+    "1. Add 'cli' to the list of authentication methods in Azure/auth_methods"
+    " in msticpyconfig.yaml ",
+    "2. run 'az login` at the start of your session.",
+    "3. Re-run the MSTICPy function that failed with this error.",
+    "",
+]
+
 
 @export
 class BHKeyVaultClient:
@@ -64,9 +78,9 @@ def __init__(
         auth_methods : List[str]
             The authentication methods to use for Key Vault auth
             Possible values are:
-            - "env" - to get authentication details from environment varibales
+            - "env" - to get authentication details from environment variables
             - "cli" - to use Azure CLI authentication details
-            - "msi" - to user Managed Service Indenity details
+            - "msi" - to user Managed Service Identity details
             - "interactive" - to prompt for interactive login
         authn_type : str, optional
             [deprecated - use auth_methods]
@@ -104,6 +118,7 @@ def __init__(
                 "Could not get TenantId from function parameters or configuration.",
                 "Please add this to the KeyVault section of msticpyconfig.yaml",
                 title="missing tenant ID value.",
+                azcli_uri="https://docs.microsoft.com/cli/azure/authenticate-azure-cli",
             )
         self.authn_type = kwargs.pop(
             "authn_type", self.settings.get("authntype", "interactive")
@@ -120,6 +135,44 @@ def __init__(
             authority_uri=kwargs.get("authority_uri"), tenant=self.tenant_id
         )
 
+        self._vault_name, self.vault_uri = self._get_vault_name_and_uri(
+            vault_name, vault_uri
+        )
+        self.kv_client = self._get_secret_client()
+
+    def _get_secret_client(self):
+        """Return the Secrets primed with credentials."""
+        # Create a secret client
+        return self._test_working_credentials()
+
+    def _test_working_credentials(self):
+        """Try to access Key Vault to establish usable authentication method."""
+        for idx, auth_method in enumerate(self.auth_methods):
+            _print_status(
+                f"Attempting connection to Key Vault using {auth_method} credentials...",
+                newline=False,
+            )
+            credentials = az_connect_core(auth_methods=[auth_method])
+            kv_client = SecretClient(self.vault_uri, credentials.modern)
+            try:
+                # need to list to force iterator to run
+                list(kv_client.list_properties_of_secrets())
+                _print_status("done")
+                return kv_client
+            except ClientAuthenticationError as client_err:
+                _print_status(f"Could not obtain access token using {auth_method}.")
+                if idx + 1 < len(self.auth_methods):
+                    continue
+                raise MsticpyUserConfigError(
+                    "No configured authentication methods found with credentials "
+                    f"with access the Key Vault '{self._vault_name}'",
+                    *_KV_CLIENT_AUTH_ERROR,
+                    title="Key Vault authentication configuration failed.",
+                ) from client_err
+        return None
+
+    def _get_vault_name_and_uri(self, vault_name, vault_uri):
+        """Validate and return vault name and URI."""
         if not vault_uri and not vault_name:
             if "vaultname" in self.settings:
                 vault_name = self.settings["vaultname"]
@@ -129,12 +182,10 @@ def __init__(
                     + " in your configuration",
                     title="Key Vault vault name not found.",
                 )
-        if vault_uri:
-            self.vault_uri = vault_uri
-        else:
+        if not vault_uri:
             vault_uri = self.settings.keyvault_uri
             if vault_uri:
-                self.vault_uri = vault_uri.format(vault=vault_name)
+                vault_uri = vault_uri.format(vault=vault_name)
             else:
                 cloud = self.settings.cloud
                 raise MsticpyKeyVaultConfigError(
@@ -144,15 +195,16 @@ def __init__(
                     title="no Key Vault URI for national cloud",
                 )
         if self.debug:
-            print(f"Using Vault URI {self.vault_uri}")
+            print(f"Using Vault URI {vault_uri}")
+        return vault_name, vault_uri
 
-        self.kv_client = self._get_secret_client()
-
-    def _get_secret_client(self):
-        credentials = az_connect_core(auth_methods=self.auth_methods)
-
-        # Create a secret client
-        return SecretClient(self.vault_uri, credentials.modern)
+    @property
+    def vault_name(self) -> str:
+        """Return the Key Vault name."""
+        return (
+            self._vault_name
+            or self.vault_uri.replace("https://", "").split(".", maxsplit=1)[0]
+        )
 
     @property
     def secrets(self):
@@ -421,3 +473,12 @@ def _get_parsed_token_data(token) -> Any:
     tok_data = tok_data.split(".")[1]
     tok_data += "=" * ((4 - len(tok_data) % 4) % 4)
     return json.loads(base64.b64decode(tok_data))
+
+
+def _print_status(message, newline=True):
+    if is_ipython():
+        line_break = "<br>" if newline else ""
+        display(HTML(f"{message}{line_break}"))
+    else:
+        line_break = "\n" if newline else ""
+        print(message, end=line_break)
diff --git a/msticpy/common/utility.py b/msticpy/common/utility.py
index b7f34a6cb..1659aabe6 100644
--- a/msticpy/common/utility.py
+++ b/msticpy/common/utility.py
@@ -14,6 +14,7 @@
 import warnings
 from enum import Enum
 from pathlib import Path
+from platform import python_version
 from typing import (
     Any,
     Callable,
@@ -26,11 +27,10 @@
     TypeVar,
     Union,
 )
-from platform import python_version
 
 import pkg_resources
 from IPython import get_ipython
-from IPython.display import HTML, display, DisplayHandle
+from IPython.display import HTML, DisplayHandle, display
 from tqdm.auto import tqdm
 from tqdm.notebook import tqdm as tqdm_notebook
 
@@ -338,10 +338,16 @@ def md_error(string: str, disp_id: Optional[DisplayHandle] = None):
 
 
 @export
-def is_ipython() -> bool:
+def is_ipython(notebook: bool = False) -> bool:
     """
     Return True if running in IPython environment.
 
+    Parameters
+    ----------
+    notebook : bool, optional
+        If notebook is true this will only return true if running
+        in a Jupyter notebook.
+
     Returns
     -------
     bool
@@ -349,7 +355,9 @@ def is_ipython() -> bool:
         otherwise False
 
     """
-    return bool(get_ipython())
+    if not notebook:
+        return bool(get_ipython())
+    return get_ipython() and type(get_ipython()).__name__.startswith("ZMQ")
 
 
 def check_kwarg(arg_name: str, legal_args: List[str]):
diff --git a/msticpy/data/browsers/mordor_browser.py b/msticpy/data/browsers/mordor_browser.py
index 6ec84ca60..cf9e0699b 100644
--- a/msticpy/data/browsers/mordor_browser.py
+++ b/msticpy/data/browsers/mordor_browser.py
@@ -5,7 +5,7 @@
 # --------------------------------------------------------------------------
 """Morder dataset browser."""
 from pprint import pformat
-from typing import Any, Dict, Iterable
+from typing import Any, Dict, Iterable, Optional
 
 import ipywidgets as widgets
 import pandas as pd
@@ -13,6 +13,7 @@
 
 from ..._version import VERSION
 from ...common.exceptions import MsticpyException
+from ...common.pkg_config import settings
 from ..drivers.mordor_driver import (
     MordorDriver,
     MordorEntry,
@@ -27,20 +28,23 @@
 class MordorBrowser:
     """Mordor browser widget."""
 
-    def __init__(self, save_folder: str = ".", use_cached: bool = True):
+    def __init__(self, save_folder: Optional[str] = None, use_cached: bool = True):
         """
         Initialize MordorBrowser control.
 
         Parameters
         ----------
         save_folder : str, optional
-            Folder to save downloaded files, by default "."
+            Folder to save downloaded files, by default reads the value
+            from settings or defaults to "."
         use_cached : bool, optional
             If true, downloaded files are not deleted after
             download and are used as a local cache, by default True
 
         """
-        self._save_folder = save_folder
+        mdr_settings = settings.get("DataProviders", {}).get("Mordor", {})
+        self._save_folder = save_folder or mdr_settings.get("save_folder", ".")
+
         self._use_cached = use_cached
         self.mordor_driver = MordorDriver()
         self.mordor_driver.connect()
@@ -226,11 +230,13 @@ def _select_ds_item(self, change):  # noqa: MC0001
                 value = None
 
             if field_attrs["type"] == "text":
-                self.fields[field].value = str(value)
+                self.fields[field].value = str(value) if value else ""
             elif field_attrs["type"] == "list":
-                self.fields[field].value = ", ".join([str(item) for item in value])
+                self.fields[field].value = (
+                    ", ".join([str(item) for item in value]) if value else ""
+                )
             elif field_attrs["type"] == "raw":
-                self.fields[field].value = pformat(value)
+                self.fields[field].value = pformat(value) if value else ""
             elif field == "attacks":
                 field_data = mdr_item.get_attacks()
                 self.fields[field].value = _format_attacks(field_data)
diff --git a/msticpy/datamodel/entities/entity.py b/msticpy/datamodel/entities/entity.py
index 8b526aef3..331041bdd 100644
--- a/msticpy/datamodel/entities/entity.py
+++ b/msticpy/datamodel/entities/entity.py
@@ -7,9 +7,9 @@
 import json
 import pprint
 import typing
-from datetime import datetime
 from abc import ABC
 from copy import deepcopy
+from datetime import datetime
 from typing import Any, Dict, List, Mapping, Optional, Type, Union
 
 import networkx as nx
@@ -86,7 +86,7 @@ def __init__(self, src_entity: Mapping[str, Any] = None, **kwargs):
         self.TimeGenerated = datetime.utcnow()
         self.Type = self._get_entity_type_name(type(self))
         # If we have an unknown entity see if we a type passed in
-        if self.Type == "unknownentity" and "Type" in kwargs:
+        if self.Type == "unknown" and "Type" in kwargs:
             self.Type = kwargs["Type"]
         # Make sure Type is in the class schema dictionary
         self._entity_schema["Type"] = None
diff --git a/msticpy/datamodel/pivots/pivot.py b/msticpy/datamodel/pivots/pivot.py
index 7c5607572..02a58a623 100644
--- a/msticpy/datamodel/pivots/pivot.py
+++ b/msticpy/datamodel/pivots/pivot.py
@@ -8,13 +8,13 @@
 from typing import Any, Callable, Dict, Iterable, Optional, Type
 
 import pkg_resources
-from IPython import get_ipython
 
 from ..._version import VERSION
 from ...common.timespan import TimeSpan
 from ...data import QueryProvider
-from ...nbtools.nbwidgets import QueryTime
 from ...data.context.tilookup import TILookup
+from ...nbtools.nbwidgets import QueryTime
+from .. import entities
 
 # pylint: disable=unused-import
 from . import pivot_pd_accessor  # noqa: F401
@@ -23,7 +23,6 @@
 from .pivot_register import PivotRegistration
 from .pivot_register_reader import add_unbound_pivot_function, register_pivots
 from .pivot_ti_provider import add_ioc_queries_to_entities, register_ti_pivot_providers
-from .. import entities
 
 __version__ = VERSION
 __author__ = "Ian Hellen"
@@ -31,11 +30,6 @@
 _DEF_PIVOT_REG_FILE = "resources/mp_pivot_reg.yaml"
 
 
-# Import IPython magic if in an IPython environment
-if get_ipython():
-    from . import txt_df_magic  # noqa: F401
-
-
 class Pivot:
     """Pivot environment loader."""
 
diff --git a/msticpy/datamodel/pivots/txt_df_magic.py b/msticpy/datamodel/pivots/txt_df_magic.py
deleted file mode 100644
index 2b312865b..000000000
--- a/msticpy/datamodel/pivots/txt_df_magic.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -------------------------------------------------------------------------
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License. See License.txt in the project root for
-# license information.
-# --------------------------------------------------------------------------
-"""Text to DataFrame IPython magic."""
-from IPython.core.magic import needs_local_scope, register_cell_magic
-from .pivot_magic_core import run_txt2df
-
-from ..._version import VERSION
-
-__version__ = VERSION
-__author__ = "Ian Hellen"
-
-
-@register_cell_magic
-@needs_local_scope
-def txt2df(line, cell, local_ns):
-    """Convert cell text to pandas DataFrame."""
-    return run_txt2df(line, cell, local_ns)
diff --git a/msticpy/datamodel/txt_df_magic.py b/msticpy/datamodel/txt_df_magic.py
deleted file mode 100644
index 706ac7991..000000000
--- a/msticpy/datamodel/txt_df_magic.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# -------------------------------------------------------------------------
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License. See License.txt in the project root for
-# license information.
-# --------------------------------------------------------------------------
-"""
-Deprecated - module txt_df_magic.py has moved.
-
-See :py:mod:`msticpy.datamodel.pivots.txt_df_magic`
-"""
-import warnings
-from .._version import VERSION
-
-__version__ = VERSION
-__author__ = "Pete Bryan"
-
-
-# flake8: noqa: F403, F401
-# pylint: disable=wildcard-import, unused-wildcard-import, unused-import
-from .pivots.txt_df_magic import *
-
-WARN_MSSG = (
-    "This module has moved to msticpy.datamodel.pivots.txt_df_magic\n"
-    "Please change your import to reflect this new location."
-    "This will be removed in MSTICPy v2.0.0"
-)
-warnings.warn(WARN_MSSG, category=DeprecationWarning)
diff --git a/msticpy/nbtools/nbinit.py b/msticpy/nbtools/nbinit.py
index 85646c8b3..35a05ecda 100644
--- a/msticpy/nbtools/nbinit.py
+++ b/msticpy/nbtools/nbinit.py
@@ -28,7 +28,7 @@
     sns = None
 
 from .._version import VERSION
-from ..common.azure_auth_core import check_cli_credentials, AzureCliStatus
+from ..common.azure_auth_core import AzureCliStatus, check_cli_credentials
 from ..common.check_version import check_version
 from ..common.exceptions import MsticpyException, MsticpyUserError
 from ..common.pkg_config import get_config, validate_config
@@ -154,6 +154,7 @@ def _verbose(verbosity: Optional[int] = None) -> int:
     dict(pkg="msticpy.common.wsconfig", tgt="WorkspaceConfig"),
     dict(pkg="msticpy.datamodel.pivots.pivot", tgt="Pivot"),
     dict(pkg="msticpy.datamodel", tgt="entities"),
+    dict(pkg="msticpy.nbtools", tgt="nbmagics"),
     dict(pkg="msticpy.vis", tgt="mp_pandas_plot"),
 ]
 _MP_IMPORT_ALL = [
diff --git a/msticpy/nbtools/nbmagics.py b/msticpy/nbtools/nbmagics.py
new file mode 100644
index 000000000..9af5d6275
--- /dev/null
+++ b/msticpy/nbtools/nbmagics.py
@@ -0,0 +1,172 @@
+# -------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+# --------------------------------------------------------------------------
+"""msticpy IPython magics."""
+import re
+from typing import List, Tuple
+
+# pylint: disable=unused-import
+# flake8: noqa: F403
+import pandas as pd
+
+# pylint: enable=unused-import
+from IPython import get_ipython
+from IPython.core import magic_arguments
+from IPython.core.magic import (
+    Magics,
+    line_cell_magic,
+    magics_class,
+    needs_local_scope,
+    register_cell_magic,
+)
+
+from ..datamodel.pivots.pivot_magic_core import run_txt2df
+
+try:
+    from bs4 import BeautifulSoup
+
+    _BS_AVAILABLE = True
+except ImportError:
+    _BS_AVAILABLE = False
+
+from .._version import VERSION
+from ..common.utility import is_ipython
+from ..sectools import base64unpack as base64
+from ..sectools.iocextract import IoCExtract
+
+__version__ = VERSION
+__author__ = "Ian Hellen"
+
+
+if is_ipython():
+
+    @magics_class
+    class Base64Magic(Magics):
+        """Base64 IPython magic extension."""
+
+        _STRIP_TAGS = r"</?decoded[^>]*>"
+
+        @line_cell_magic
+        @magic_arguments.magic_arguments()
+        @magic_arguments.argument(
+            "--out", "-o", help="The variable to return the results in"
+        )
+        @magic_arguments.argument(
+            "--pretty",
+            "-p",
+            help="Print formatted version of output",
+            action="store_true",
+        )
+        @magic_arguments.argument(
+            "--clean",
+            "-c",
+            help="Print decoded string with no formatting",
+            action="store_true",
+        )
+        def b64(self, line: str = "", cell: str = None) -> str:
+            """
+            Base64 IPython magic extension.
+
+            Parameters
+            ----------
+            line : str, optional
+                Line contents, by default ""
+            cell : str, optional
+                Cell contents, by default None
+
+            Returns
+            -------
+            str
+                Decoded text
+
+            """
+            if cell is None:
+                results, df_results = base64.unpack(line)
+
+            else:
+                results, df_results = base64.unpack(cell)
+            args = magic_arguments.parse_argstring(self.b64, line)
+
+            if args.clean:
+                results = re.sub(self._STRIP_TAGS, "", results)
+            elif args.pretty:
+                if _BS_AVAILABLE:
+                    xml_str = f"<decoded_string>{results}</decoded_string>"
+                    b_soup = BeautifulSoup(xml_str, "xml")
+                    results = b_soup.prettify()
+            if args.out is not None:
+                self.shell.user_ns[args.out] = (results, df_results)
+            return results
+
+    @magics_class
+    class IoCExtractMagic(Magics):
+        """Ioc Extract IPython magic extension."""
+
+        def __init__(self, shell):
+            """
+            Instantiate magic class.
+
+            Parameters
+            ----------
+            shell : IPython shell
+                IPython shell
+
+            """
+            # You must call the parent constructor
+            super().__init__(shell)
+            self._ioc_extract = IoCExtract()
+
+        @line_cell_magic
+        @magic_arguments.magic_arguments()
+        @magic_arguments.argument(
+            "--out", "-o", help="The variable to return the results in"
+        )
+        @magic_arguments.argument(
+            "--ioc_types",
+            "-i",
+            help="The types of IoC to search for (comma-separated string)",
+        )
+        def ioc(self, line="", cell=None) -> List[Tuple[str, List[str]]]:
+            """
+            Ioc Extract IPython magic extension.
+
+            Parameters
+            ----------
+            line : str, optional
+                Line contents, by default ""
+            cell : str, optional
+                Cell contents, by default None
+
+            Returns
+            -------
+            List[Tuple[str, List[str]]]
+                List of tuples of IoCs found grouped by type.
+
+            """
+            args = magic_arguments.parse_argstring(self.ioc, line)
+            ioc_types = None
+            if args.ioc_types:
+                ioc_types = [ioc_type.strip() for ioc_type in args.ioc_types.split(",")]
+
+            if cell is None:
+                results = self._ioc_extract.extract(src=line, ioc_types=ioc_types)
+            else:
+                results = self._ioc_extract.extract(src=cell, ioc_types=ioc_types)
+            iocs = [(ioc_type, list(ioc_res)) for ioc_type, ioc_res in results.items()]
+
+            if args.out is not None:
+                self.shell.user_ns[args.out] = results
+            return iocs
+
+    IPYTHON = get_ipython()
+    if IPYTHON:
+        IPYTHON.register_magics(Base64Magic)
+        IPYTHON.register_magics(IoCExtractMagic)
+
+    @register_cell_magic
+    @needs_local_scope
+    def txt2df(line, cell, local_ns):
+        """Convert cell text to pandas DataFrame."""
+        return run_txt2df(line, cell, local_ns)
diff --git a/msticpy/sectools/__init__.py b/msticpy/sectools/__init__.py
index d888316a3..659bb317c 100644
--- a/msticpy/sectools/__init__.py
+++ b/msticpy/sectools/__init__.py
@@ -32,21 +32,15 @@
 
 """
 
+# from . import process_tree_utils as ptree
+from .._version import VERSION
+from ..analysis.data import base64unpack as base64
+
 # flake8: noqa: F403
 # pylint: disable=W0401
 from ..analysis.data.iocextract import IoCExtract
 from ..data.context.geoip import GeoLiteLookup, IPStackLookup, geo_distance
 from ..data.context.tilookup import TILookup
 from ..data.context.tiproviders.vtlookupv3.vtlookup import VTLookup
-from ..analysis.data import base64unpack as base64
-
-# from . import process_tree_utils as ptree
-from .._version import VERSION
-
-try:
-    from IPython import get_ipython
-    from .analysis import sectools_magics
-except ImportError as err:
-    pass
 
 __version__ = VERSION
diff --git a/msticpy/sectools/sectools_magics.py b/msticpy/sectools/sectools_magics.py
index 98e6e37ef..0c1e8dbd7 100644
--- a/msticpy/sectools/sectools_magics.py
+++ b/msticpy/sectools/sectools_magics.py
@@ -9,6 +9,7 @@
 See :py:mod:`msticpy.analysis.sectools_magics`
 """
 import warnings
+
 from .._version import VERSION
 
 __version__ = VERSION
@@ -17,7 +18,7 @@
 
 # flake8: noqa: F403, F401
 # pylint: disable=wildcard-import, unused-wildcard-import, unused-import
-from ..analysis.sectools_magics import *
+from ..nbtools import nbmagics
 
 WARN_MSSG = (
     "This module has moved to msticpy.analysis.sectools_magics\n"
diff --git a/msticpy/sectools/vtlookupv3/__init__.py b/msticpy/sectools/vtlookupv3/__init__.py
index 80ba8885e..fb30c7aeb 100644
--- a/msticpy/sectools/vtlookupv3/__init__.py
+++ b/msticpy/sectools/vtlookupv3/__init__.py
@@ -11,7 +11,13 @@
 # flake8: noqa: F401
 from .vt_pivot import add_pivot_functions
 from .vtfile_behavior import VTFileBehavior
-from .vtlookupv3 import VTLookupV3, VT_API_NOT_FOUND, MsticpyVTNoDataError
+from .vtlookupv3 import (
+    VTLookupV3,
+    VT_API_NOT_FOUND,
+    MsticpyVTNoDataError,
+    VTEntityType,
+    VTObjectProperties,
+)
 from .vtobject_browser import VTObjectBrowser
 
 __version__ = VERSION
diff --git a/requirements-all.txt b/requirements-all.txt
index 520fedcde..067278666 100644
--- a/requirements-all.txt
+++ b/requirements-all.txt
@@ -21,11 +21,12 @@ geoip2>=2.9.0
 httpx>=0.21
 html5lib
 ipwhois>=1.1.0
-ipython>=7.23.1
+ipython >= 7.1.1; python_version < "3.8"
+ipython >= 7.23.1; python_version >= "3.8"
 ipywidgets>=7.4.2
 keyring>=13.2.1
-KqlmagicCustom[jupyter-basic,auth_code_clipboard]>=0.1.114.dev26
-KqlmagicCustom[jupyter-extended]>=0.1.114.dev25
+KqlmagicCustom[jupyter-basic,auth_code_clipboard]>=0.1.114
+KqlmagicCustom[jupyter-extended]>=0.1.114
 lxml>=4.6.3
 matplotlib>=3.0.0
 moz_sql_parser>=4.5.0,<=4.11.21016
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 691cfc173..a54a07ccb 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -3,6 +3,7 @@ bandit>=1.7.0
 beautifulsoup4
 black>=20.8b1
 coverage>=5.5
+docutils<0.18.0
 filelock>=3.0.0
 flake8>=3.8.4
 markdown>=3.3.4
@@ -23,7 +24,8 @@ pyroma>=3.1
 pytest-check>=1.0.1
 pytest-cov>=2.11.1
 pytest>=5.0.1
-respx>=0.19.0
-sphinx>=2.1.2
-sphinx_rtd_theme>=0.5.1
-virtualenv
+readthedocs-sphinx-ext==2.1.4
+responses>=0.13.2
+respx==0.19.2
+sphinx-rtd-theme>=1.0.0
+sphinx>=4.4.0
diff --git a/requirements.txt b/requirements.txt
index ed06ea144..be745c8a0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,10 +12,11 @@ geoip2>=2.9.0
 httpx>=0.21
 html5lib
 ipwhois>=1.1.0
-ipython>=7.23.1
+ipython >= 7.23.1; python_version >= "3.8"
+ipython >= 7.1.1; python_version < "3.8"
 ipywidgets>=7.4.2
 # keyring>=13.2.1  # azure
-KqlmagicCustom[jupyter-basic,auth_code_clipboard]>=0.1.114.dev26
+KqlmagicCustom[jupyter-basic,auth_code_clipboard]>=0.1.114
 lxml>=4.6.3
 matplotlib>=3.0.0
 msrest>=0.6.0
diff --git a/setup.py b/setup.py
index d87b0c05b..bac17aa39 100644
--- a/setup.py
+++ b/setup.py
@@ -30,7 +30,7 @@ def _combine_extras(extras: list) -> list:
     "vt3": ["vt-py>=0.6.1", "nest_asyncio>=1.4.0"],
     "splunk": ["splunk-sdk>=1.6.0"],
     "sumologic": ["sumologic-sdk>=0.1.11", "openpyxl>=3.0"],
-    "kql": ["KqlmagicCustom[jupyter-extended]>=0.1.114.dev25"],
+    "kql": ["KqlmagicCustom[jupyter-extended]>=0.1.114"],
     "_azure_core": [
         "azure-mgmt-compute>=4.6.2",
         "azure-mgmt-core>=1.2.1",
diff --git a/tools/toollib/import_analyzer.py b/tools/toollib/import_analyzer.py
index 20a14b944..5a633924f 100644
--- a/tools/toollib/import_analyzer.py
+++ b/tools/toollib/import_analyzer.py
@@ -4,12 +4,12 @@
 # license information.
 # --------------------------------------------------------------------------
 """Python file import analyzer."""
-import re
 import sys
 from importlib import import_module
 from pathlib import Path
-from typing import Dict, List, Optional, Set
+from typing import Any, Dict, List, Optional, Set, Tuple
 
+import pkg_resources
 import networkx as nx
 
 from . import VERSION
@@ -54,12 +54,34 @@ def __init__(self):
 }
 
 
-def _get_setup_reqs(
+def get_setup_reqs(
     package_root: str,
     req_file="requirements.txt",
     extras: Optional[List[str]] = None,
     skip_setup=True,
-):
+) -> Tuple[Dict[str, Any], Dict[str, str]]:
+    """
+    Return list of extras from setup.py.
+
+    Parameters
+    ----------
+    package_root : str
+        The root folder of the package
+    req_file
+        Requirements file
+    extras : Optional[List[str]], optional
+        Optional list of extras to process in place of reading
+        from setup CFG
+    skip_setup : bool, optional
+        If True, process the setup file, by default True
+
+    Returns
+    -------
+    Tuple[Dict[str, SpecifierSet], Dict[str, str]]
+        Tuple of Dict[pkg_name_lower, pkg_name], Dict[pkg_name, version_spec]
+        of package requirements.
+
+    """
     with open(Path(package_root).joinpath(req_file), "r", encoding="utf-8") as req_f:
         req_list = req_f.readlines()
 
@@ -71,8 +93,16 @@ def _get_setup_reqs(
             setup_pkgs = setup_pkgs | extra_pkgs
         except ImportError:
             print("Could not process modifed 'setup.py'")
-    setup_versions = {key[0].casefold(): key for key in setup_pkgs}
-    setup_reqs = {key[0].casefold(): key[0] for key in setup_pkgs}
+    setup_versions = {
+        req.name.casefold(): req.specifier
+        for req in setup_pkgs
+        if req.marker is None or req.marker.evaluate()
+    }
+    setup_reqs = {
+        req.name.casefold(): req.name
+        for req in setup_pkgs
+        if req.marker is None or req.marker.evaluate()
+    }
 
     # for packages that do not match top-level names
     # add the mapping
@@ -125,9 +155,9 @@ def get_extras_from_setup(
 
 def _extract_pkg_specs(pkg_specs: List[str]):
     return {
-        re.match(PKG_TOKENS, item).groups()  # type: ignore
-        for item in pkg_specs
-        if re.match(PKG_TOKENS, item) and not item.strip().startswith("#")
+        pkg_resources.Requirement.parse(req)  # type: ignore
+        for req in pkg_specs
+        if (req and not req.strip().startswith("#"))
     }
 
 
@@ -275,7 +305,7 @@ def analyze_imports(
         A dictionary of modules and imports
 
     """
-    setup_reqs, _ = _get_setup_reqs(
+    setup_reqs, _ = get_setup_reqs(
         package_root, req_file, extras, skip_setup=(not process_setup_py)
     )
     pkg_root = Path(package_root) / package_name


Test patch:
diff --git a/tests/test_pkg_imports.py b/tests/test_pkg_imports.py
index 81cd83239..308baea4e 100644
--- a/tests/test_pkg_imports.py
+++ b/tests/test_pkg_imports.py
@@ -8,11 +8,12 @@
 import re
 import sys
 from pathlib import Path
+import pkg_resources
 
 import pytest
 import pytest_check as check
 
-from tools.toollib.import_analyzer import analyze_imports, get_extras_from_setup
+from tools.toollib.import_analyzer import analyze_imports, get_setup_reqs
 
 PKG_ROOT = "."
 PKG_NAME = "msticpy"
@@ -41,7 +42,7 @@
 @pytest.fixture(scope="module")
 def extras_from_setup():
     """Read extras packages from setup.py."""
-    return get_extras_from_setup(extra="all")
+    return get_setup_reqs(PKG_ROOT, skip_setup=False)[1]
 
 
 def test_missing_pkgs_req():
@@ -80,51 +81,60 @@ def test_missing_pkgs_req():
 
 def test_conda_reqs(extras_from_setup):
     """Test conda requirements files match main file."""
-    main_reqs_file = Path(PKG_ROOT) / REQS_FILE
     conda_reqs_file = Path(PKG_ROOT) / "conda/conda-reqs.txt"
     conda_reqs_pip_file = Path(PKG_ROOT) / "conda/conda-reqs-pip.txt"
 
-    main_reqs_dict = _get_reqs_from_file(main_reqs_file)
-    # Add extras
-    for item in [re.split(REQS_OP_RGX, line) for line in extras_from_setup]:
-        main_reqs_dict[item[0].strip()] = item[1].strip() if len(item) > 1 else None
+    # main_reqs_dict = _get_reqs_from_file(main_reqs_file)
+    # # Add extras
+    # for item in [re.split(REQS_OP_RGX, line) for line in extras_from_setup]:
+    #     main_reqs_dict[item[0].strip()] = item[1].strip() if len(item) > 1 else None
 
     conda_reqs_dict = _get_reqs_from_file(conda_reqs_file)
     conda_reqs_pip_dict = _get_reqs_from_file(conda_reqs_pip_file)
 
-    for key, val in main_reqs_dict.items():
+    for pkg_name, pkg_specifier in extras_from_setup.items():
         # print(f"Checking {key} in conda-reqs.txt", bool(key in conda_reqs_dict))
         # print(f"Checking {key} in conda-reqs-pip.txt", bool(key in conda_reqs_pip_dict))
 
         if (
-            key not in conda_reqs_dict
-            and key not in conda_reqs_pip_dict
-            and key not in CONDA_PKG_EXCEPTIONS
+            pkg_name not in conda_reqs_dict
+            and pkg_name not in conda_reqs_pip_dict
+            and pkg_name not in CONDA_PKG_EXCEPTIONS
         ):
-            print(f"Test Error - no conda package equiv for {key}=={val}")
+            print(
+                f"Test Error - no conda package equiv for {pkg_name}=={pkg_specifier}"
+            )
         check.is_true(
-            key in conda_reqs_dict
-            or key in conda_reqs_pip_dict
-            or key in CONDA_PKG_EXCEPTIONS,
+            pkg_name in conda_reqs_dict
+            or pkg_name in conda_reqs_pip_dict
+            or pkg_name in CONDA_PKG_EXCEPTIONS,
         )
-        if key in conda_reqs_dict:
-            if conda_reqs_dict[key]:
-                if val != conda_reqs_dict[key]:
+        if pkg_name in conda_reqs_dict:
+            if conda_reqs_dict[pkg_name]:
+                if pkg_specifier != conda_reqs_dict[pkg_name]:
                     print(
-                        f"{key} version mismatch - setup: {val}: {conda_reqs_dict[key]}",
+                        f"{pkg_name} version mismatch - setup: {pkg_specifier}: {conda_reqs_dict[pkg_name]}",
                         "in conda-reqs.txt",
                     )
-                check.equal(val, conda_reqs_dict[key], f"{key} in condas reqs")
-            conda_reqs_dict.pop(key)
-        if key in conda_reqs_pip_dict:
-            if conda_reqs_pip_dict[key]:
-                if val != conda_reqs_pip_dict[key]:
+                check.equal(
+                    pkg_specifier,
+                    conda_reqs_dict[pkg_name],
+                    f"{pkg_name} in condas reqs",
+                )
+            conda_reqs_dict.pop(pkg_name)
+        if pkg_name in conda_reqs_pip_dict:
+            if conda_reqs_pip_dict[pkg_name]:
+                if pkg_specifier != conda_reqs_pip_dict[pkg_name]:
                     print(
-                        f"{key} version mismatch - setup: {val}: {conda_reqs_pip_dict[key]}",
+                        f"{pkg_name} version mismatch - setup: {pkg_specifier}: {conda_reqs_pip_dict[pkg_name]}",
                         "in conda-reqs-pip.txt",
                     )
-                check.equal(val, conda_reqs_pip_dict[key], f"{key} in condas pip reqs")
-            conda_reqs_pip_dict.pop(key)
+                check.equal(
+                    pkg_specifier,
+                    conda_reqs_pip_dict[pkg_name],
+                    f"{pkg_name} in condas pip reqs",
+                )
+            conda_reqs_pip_dict.pop(pkg_name)
 
     if conda_reqs_dict:
         print("Extra items found in conda-reqs.txt", conda_reqs_pip_dict)
@@ -135,12 +145,11 @@ def test_conda_reqs(extras_from_setup):
 
 
 def _get_reqs_from_file(reqs_file):
-    conda_reqs_dict = {}
     with open(str(reqs_file), "r") as f_hdl:
-        reqs = f_hdl.readlines()
-        lines = [line for line in reqs if not line.strip().startswith("#")]
-        for item in [re.split(REQS_OP_RGX, line) for line in lines]:
-            conda_reqs_dict[item[0].strip()] = (
-                item[1].strip() if len(item) > 1 else None
-            )
-    return conda_reqs_dict
+        reqs_lines = f_hdl.readlines()
+    reqs = [
+        pkg_resources.Requirement.parse(req)
+        for req in reqs_lines
+        if req.strip() and not req.strip().startswith("#")
+    ]
+    return {req.name.casefold(): req.specifier for req in reqs}