azdev 0.1.72__tar.gz → 0.1.74__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {azdev-0.1.72 → azdev-0.1.74}/HISTORY.rst +9 -0
- {azdev-0.1.72/azdev.egg-info → azdev-0.1.74}/PKG-INFO +11 -1
- {azdev-0.1.72 → azdev-0.1.74}/azdev/__init__.py +1 -1
- {azdev-0.1.72 → azdev-0.1.74}/azdev/commands.py +5 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/help.py +46 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/command_change/__init__.py +12 -2
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/command_change/custom.py +15 -9
- azdev-0.1.74/azdev/operations/secret.py +247 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/params.py +29 -0
- {azdev-0.1.72 → azdev-0.1.74/azdev.egg-info}/PKG-INFO +11 -1
- {azdev-0.1.72 → azdev-0.1.74}/azdev.egg-info/SOURCES.txt +1 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev.egg-info/requires.txt +1 -0
- {azdev-0.1.72 → azdev-0.1.74}/setup.py +2 -1
- {azdev-0.1.72 → azdev-0.1.74}/LICENSE +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/MANIFEST.in +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/README.md +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/README.rst +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/__main__.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/completer.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/config/__init__.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/config/cli.flake8 +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/config/cli_pylintrc +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/config/ext.flake8 +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/config/ext_pylintrc +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/mod_templates/HISTORY.rst +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/mod_templates/README.rst +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/mod_templates/_client_factory.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/mod_templates/_help.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/mod_templates/_params.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/mod_templates/_validators.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/mod_templates/azext_metadata.json +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/mod_templates/blank__init__.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/mod_templates/commands.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/mod_templates/custom.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/mod_templates/module__init__.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/mod_templates/pkg_declare__init__.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/mod_templates/setup.cfg +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/mod_templates/setup.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/mod_templates/test_service_scenario.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/__init__.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/cmdcov/__init__.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/cmdcov/_macros.j2 +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/cmdcov/cmdcov.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/cmdcov/component.css +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/cmdcov/component.js +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/cmdcov/favicon.ico +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/cmdcov/index.j2 +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/cmdcov/index2.j2 +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/cmdcov/module.j2 +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/code_gen.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/command_change/util.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/constant.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/extensions/__init__.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/extensions/util.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/extensions/version_upgrade.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/help/__init__.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/help/refdoc/__init__.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/help/refdoc/conf.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/legal.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/linter/__init__.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/linter/linter.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/linter/pylint_checkers/__init__.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/linter/pylint_checkers/show_command.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/linter/rule_decorators.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/linter/rules/__init__.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/linter/rules/ci_exclusions.yml +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/linter/rules/command_coverage_rules.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/linter/rules/command_group_rules.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/linter/rules/command_rules.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/linter/rules/help_rules.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/linter/rules/linter_exclusions.yml +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/linter/rules/parameter_rules.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/linter/util.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/performance.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/pypi.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/python_sdk.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/regex.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/resource.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/setup.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/statistics/__init__.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/statistics/util.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/style.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/testtool/__init__.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/testtool/incremental_strategy.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/testtool/profile_context.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/operations/testtool/pytest_runner.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/transformers.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/utilities/__init__.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/utilities/command.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/utilities/config.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/utilities/const.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/utilities/display.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/utilities/git_util.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/utilities/path.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/utilities/pypi.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/utilities/testing.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev/utilities/tools.py +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev.egg-info/dependency_links.txt +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev.egg-info/entry_points.txt +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/azdev.egg-info/top_level.txt +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/pyproject.toml +0 -0
- {azdev-0.1.72 → azdev-0.1.74}/setup.cfg +0 -0
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Release History
|
|
4
4
|
===============
|
|
5
|
+
0.1.74
|
|
6
|
+
++++++
|
|
7
|
+
* `azdev scan/mask`: New commands for scanning and masking secrets for files or string
|
|
8
|
+
|
|
9
|
+
0.1.73
|
|
10
|
+
++++++
|
|
11
|
+
* `azdev command-change meta-export`: Add `has_completer` to denote whether completer is configed in arg
|
|
12
|
+
* `azdev command-change meta-export`: Extracting arg help and example for loaded HelpFiles
|
|
13
|
+
|
|
5
14
|
0.1.72
|
|
6
15
|
++++++
|
|
7
16
|
* Bump `pylint` to 3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: azdev
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.74
|
|
4
4
|
Summary: Microsoft Azure CLI Developer Tools
|
|
5
5
|
Home-page: https://github.com/Azure/azure-cli-dev-tools
|
|
6
6
|
Author: Microsoft Corporation
|
|
@@ -40,6 +40,7 @@ Requires-Dist: azure-cli-diff-tool~=0.0.6
|
|
|
40
40
|
Requires-Dist: packaging
|
|
41
41
|
Requires-Dist: tqdm
|
|
42
42
|
Requires-Dist: wheel==0.30.0
|
|
43
|
+
Requires-Dist: microsoft-security-utilities-secret-masker
|
|
43
44
|
|
|
44
45
|
Microsoft Azure CLI Dev Tools (azdev)
|
|
45
46
|
=====================================
|
|
@@ -145,6 +146,15 @@ License
|
|
|
145
146
|
|
|
146
147
|
Release History
|
|
147
148
|
===============
|
|
149
|
+
0.1.74
|
|
150
|
+
++++++
|
|
151
|
+
* `azdev scan/mask`: New commands for scanning and masking secrets for files or string
|
|
152
|
+
|
|
153
|
+
0.1.73
|
|
154
|
+
++++++
|
|
155
|
+
* `azdev command-change meta-export`: Add `has_completer` to denote whether completer is configed in arg
|
|
156
|
+
* `azdev command-change meta-export`: Extracting arg help and example for loaded HelpFiles
|
|
157
|
+
|
|
148
158
|
0.1.72
|
|
149
159
|
++++++
|
|
150
160
|
* Bump `pylint` to 3
|
|
@@ -9,6 +9,7 @@ from knack.commands import CommandGroup
|
|
|
9
9
|
from .transformers import performance_benchmark_data_transformer
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
# pylint: disable=too-many-statements
|
|
12
13
|
def load_command_table(self, _):
|
|
13
14
|
|
|
14
15
|
def operation_group(name):
|
|
@@ -27,6 +28,10 @@ def load_command_table(self, _):
|
|
|
27
28
|
with CommandGroup(self, '', operation_group('linter')) as g:
|
|
28
29
|
g.command('linter', 'run_linter')
|
|
29
30
|
|
|
31
|
+
with CommandGroup(self, '', operation_group('secret')) as g:
|
|
32
|
+
g.command('scan', 'scan_secrets')
|
|
33
|
+
g.command('mask', 'mask_secrets')
|
|
34
|
+
|
|
30
35
|
with CommandGroup(self, 'statistics', operation_group('statistics')) as g:
|
|
31
36
|
g.command('list-command-table', 'list_command_table')
|
|
32
37
|
g.command('diff-command-tables', 'diff_command_tables')
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
# -----------------------------------------------------------------------------
|
|
6
6
|
|
|
7
7
|
from knack.help_files import helps
|
|
8
|
+
# pylint: disable=line-too-long, anomalous-backslash-in-string
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
helps[''] = """
|
|
@@ -159,6 +160,51 @@ helps['linter'] = """
|
|
|
159
160
|
text: azdev linter --repo azure-cli --tgt upstream/master --src upstream/dev
|
|
160
161
|
"""
|
|
161
162
|
|
|
163
|
+
helps['scan'] = """
|
|
164
|
+
short-summary: Scan secrets for files or string
|
|
165
|
+
long-summary: Check built-in scanning rules at https://github.com/microsoft/security-utilities/blob/main/GeneratedRegexPatterns/PreciselyClassifiedSecurityKeys.json
|
|
166
|
+
examples:
|
|
167
|
+
- name: Scan secrets for a single file with custom patterns
|
|
168
|
+
text: |
|
|
169
|
+
azdev scan --file-path my_file.yaml --custom-pattern my_pattern.json
|
|
170
|
+
("my_pattern.json" contains the following content)
|
|
171
|
+
{
|
|
172
|
+
"Include": [
|
|
173
|
+
{
|
|
174
|
+
"Pattern": "(?<refine>[\w.%#+-]+)(%40|@)([a-z0-9.-]*.[a-z]{2,})",
|
|
175
|
+
"Name": "EmailAddress",
|
|
176
|
+
"Signatures": ["%40", "@"]
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"Pattern": "(?<refine>[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12})",
|
|
180
|
+
"Name": "GUID"
|
|
181
|
+
}
|
|
182
|
+
],
|
|
183
|
+
"Exclude": [
|
|
184
|
+
{
|
|
185
|
+
"Id": "SEC101/156",
|
|
186
|
+
"Name": "AadClientAppIdentifiableCredentials",
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
- name: Scan secrets for raw string and save results to file
|
|
191
|
+
text: |
|
|
192
|
+
azdev scan --data "my string waiting to be scanned" --save-scan-result True
|
|
193
|
+
- name: Recursively scan secrets for a directory and save results to specific file
|
|
194
|
+
text: |
|
|
195
|
+
azdev scan --directory-path /path/to/my/folder --recursive --scan-result-path /path/to/scan_result.json
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
helps['mask'] = """
|
|
199
|
+
short-summary: Mask secrets for files or string
|
|
200
|
+
long-summary: |
|
|
201
|
+
Redaction type 'FIXED_VALUE' will mask all secrets with '***'.
|
|
202
|
+
Redaction type 'FIXED_LENGTH' will mask secrets with several '*'s which will keep the original secret length.
|
|
203
|
+
Redaction type 'SECRET_NAME' redaction type will mask secrets with their secret name (type).
|
|
204
|
+
Redaction type 'CUSTOM' will mask secrets with 'redaction_token' value you specify through saved scan result file.
|
|
205
|
+
Check built-in scanning rules at https://github.com/microsoft/security-utilities/blob/main/GeneratedRegexPatterns/PreciselyClassifiedSecurityKeys.json
|
|
206
|
+
"""
|
|
207
|
+
|
|
162
208
|
helps['statistics'] = """
|
|
163
209
|
short-summary: Commands for CLI modules statistics.
|
|
164
210
|
"""
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# license information.
|
|
5
5
|
# -----------------------------------------------------------------------------
|
|
6
6
|
|
|
7
|
-
# pylint: disable=no-else-return, too-many-nested-blocks
|
|
7
|
+
# pylint: disable=no-else-return, too-many-nested-blocks, too-many-locals, too-many-branches
|
|
8
8
|
|
|
9
9
|
import time
|
|
10
10
|
|
|
@@ -72,6 +72,16 @@ def export_command_meta(modules=None, git_source=None, git_target=None, git_repo
|
|
|
72
72
|
display('Commands loaded in {} sec'.format(stop - start))
|
|
73
73
|
command_loader = az_cli.invocation.commands_loader
|
|
74
74
|
|
|
75
|
+
from azure.cli.core.file_util import get_all_help
|
|
76
|
+
|
|
77
|
+
help_info = {}
|
|
78
|
+
if with_help or with_example:
|
|
79
|
+
help_files = get_all_help(az_cli)
|
|
80
|
+
for help_item in help_files:
|
|
81
|
+
if not help_item.command:
|
|
82
|
+
continue
|
|
83
|
+
help_info[help_item.command] = help_item
|
|
84
|
+
|
|
75
85
|
# trim command table to selected_modules
|
|
76
86
|
command_loader = filter_modules(command_loader, modules=selected_mod_names)
|
|
77
87
|
|
|
@@ -85,7 +95,7 @@ def export_command_meta(modules=None, git_source=None, git_target=None, git_repo
|
|
|
85
95
|
"name": command_name,
|
|
86
96
|
"source": _get_command_source(command_name, command),
|
|
87
97
|
"is_aaz": False,
|
|
88
|
-
"help":
|
|
98
|
+
"help": help_info[command_name] if command_name in help_info else None,
|
|
89
99
|
"confirmation": command.confirmation is True,
|
|
90
100
|
"arguments": [],
|
|
91
101
|
"az_arguments_schema": None,
|
|
@@ -119,6 +119,15 @@ def normalize_para_types(para):
|
|
|
119
119
|
normalize_para_type(type_bool_opts, "bool")
|
|
120
120
|
|
|
121
121
|
|
|
122
|
+
def get_command_examples(command_info, command_meta):
|
|
123
|
+
example_items = []
|
|
124
|
+
if command_info and command_info.get("help", None) and hasattr(command_info["help"], "examples"):
|
|
125
|
+
for example_obj in command_info["help"].examples:
|
|
126
|
+
example_items.append({"name": example_obj.name, "text": example_obj.text})
|
|
127
|
+
if example_items:
|
|
128
|
+
command_meta["examples"] = example_items
|
|
129
|
+
|
|
130
|
+
|
|
122
131
|
def gen_command_meta(command_info, with_help=False, with_example=False):
|
|
123
132
|
stored_property_when_exist = ["confirmation", "supports_no_wait", "is_preview", "deprecate_info"]
|
|
124
133
|
command_meta = {
|
|
@@ -129,15 +138,10 @@ def gen_command_meta(command_info, with_help=False, with_example=False):
|
|
|
129
138
|
if command_info.get(prop, None):
|
|
130
139
|
command_meta[prop] = command_info[prop]
|
|
131
140
|
if with_example:
|
|
132
|
-
|
|
133
|
-
command_meta["examples"] = command_info["help"]["examples"]
|
|
134
|
-
except AttributeError:
|
|
135
|
-
pass
|
|
141
|
+
get_command_examples(command_info, command_meta)
|
|
136
142
|
if with_help:
|
|
137
|
-
|
|
138
|
-
command_meta["desc"] = command_info["help"]
|
|
139
|
-
except AttributeError:
|
|
140
|
-
pass
|
|
143
|
+
if command_info.get("help", None) and hasattr(command_info["help"], "short_summary"):
|
|
144
|
+
command_meta["desc"] = command_info["help"].short_summary
|
|
141
145
|
parameters = []
|
|
142
146
|
for _, argument in command_info["arguments"].items():
|
|
143
147
|
if argument.type is None:
|
|
@@ -163,13 +167,15 @@ def gen_command_meta(command_info, with_help=False, with_example=False):
|
|
|
163
167
|
para["id_part"] = settings["id_part"]
|
|
164
168
|
if settings.get("nargs", None):
|
|
165
169
|
para["nargs"] = settings["nargs"]
|
|
170
|
+
if settings.get("completer", None):
|
|
171
|
+
para["has_completer"] = True
|
|
166
172
|
if settings.get("default", None):
|
|
167
173
|
if not isinstance(settings["default"], (float, int, str, list, bool)):
|
|
168
174
|
para["default"] = str(settings["default"])
|
|
169
175
|
else:
|
|
170
176
|
para["default"] = settings["default"]
|
|
171
177
|
if with_help:
|
|
172
|
-
para["desc"] = settings
|
|
178
|
+
para["desc"] = settings.get("help", "")
|
|
173
179
|
if command_info["is_aaz"] and command_info["az_arguments_schema"]:
|
|
174
180
|
process_aaz_argument(command_info["az_arguments_schema"], settings, para)
|
|
175
181
|
normalize_para_types(para)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
|
+
# license information.
|
|
5
|
+
# -----------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
from json.decoder import JSONDecodeError
|
|
10
|
+
from knack.log import get_logger
|
|
11
|
+
from microsoft_security_utilities_secret_masker import (load_regex_patterns_from_json_file,
|
|
12
|
+
load_regex_pattern_from_json,
|
|
13
|
+
SecretMasker)
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _validate_data_path(file_path=None, directory_path=None, data=None):
|
|
18
|
+
if file_path and directory_path:
|
|
19
|
+
raise ValueError('Can not specify file path and directory path at the same time')
|
|
20
|
+
if file_path and data:
|
|
21
|
+
raise ValueError('Can not specify file path and raw string at the same time')
|
|
22
|
+
if directory_path and data:
|
|
23
|
+
raise ValueError('Can not specify directory path and raw string at the same time')
|
|
24
|
+
if not file_path and not directory_path and not data:
|
|
25
|
+
raise ValueError('No file path or directory path or raw string provided')
|
|
26
|
+
|
|
27
|
+
if directory_path and not os.path.isdir(directory_path):
|
|
28
|
+
raise ValueError(f'invalid directory path:{directory_path}')
|
|
29
|
+
if file_path and not os.path.isfile(file_path):
|
|
30
|
+
raise ValueError(f'invalid file path:{file_path}')
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _load_built_in_regex_patterns():
|
|
34
|
+
return load_regex_patterns_from_json_file('PreciselyClassifiedSecurityKeys.json')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load_regex_patterns(custom_pattern=None):
|
|
38
|
+
built_in_regex_patterns = _load_built_in_regex_patterns()
|
|
39
|
+
|
|
40
|
+
if not custom_pattern:
|
|
41
|
+
return built_in_regex_patterns
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
if os.path.isfile(custom_pattern):
|
|
45
|
+
custom_pattern = json.load(custom_pattern)
|
|
46
|
+
else:
|
|
47
|
+
custom_pattern = json.loads(custom_pattern)
|
|
48
|
+
except JSONDecodeError as err:
|
|
49
|
+
raise ValueError(f'Custom pattern should be in valid json format, err:{err.msg}')
|
|
50
|
+
|
|
51
|
+
regex_patterns = []
|
|
52
|
+
if 'Include' in custom_pattern:
|
|
53
|
+
for pattern in custom_pattern['Include']:
|
|
54
|
+
if not pattern.get('Pattern', None):
|
|
55
|
+
raise ValueError(f'Invalid Custom Pattern: {pattern}, '
|
|
56
|
+
f'"Pattern" property is required for Include patterns')
|
|
57
|
+
regex_patterns.append(load_regex_pattern_from_json(pattern))
|
|
58
|
+
if "Exclude" in custom_pattern:
|
|
59
|
+
exclude_pattern_ids = []
|
|
60
|
+
for pattern in custom_pattern['Exclude']:
|
|
61
|
+
if not pattern.get('Id', None):
|
|
62
|
+
raise ValueError(f'Invalid Custom Pattern: {pattern}, "Id" property is required for Exclude patterns')
|
|
63
|
+
exclude_pattern_ids.append(pattern['Id'])
|
|
64
|
+
for pattern in built_in_regex_patterns:
|
|
65
|
+
if pattern.id in exclude_pattern_ids:
|
|
66
|
+
continue
|
|
67
|
+
regex_patterns.append(pattern)
|
|
68
|
+
else:
|
|
69
|
+
regex_patterns.extend(built_in_regex_patterns)
|
|
70
|
+
return regex_patterns
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _scan_secrets_for_string(data, custom_pattern=None):
|
|
74
|
+
if not data:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
regex_patterns = _load_regex_patterns(custom_pattern)
|
|
78
|
+
secret_masker = SecretMasker(regex_patterns)
|
|
79
|
+
detected_secrets = secret_masker.detect_secrets(data)
|
|
80
|
+
secrets = []
|
|
81
|
+
for secret in detected_secrets:
|
|
82
|
+
secrets.append({
|
|
83
|
+
'secret_name': secret.name,
|
|
84
|
+
'secret_value': data[secret.start:secret.end],
|
|
85
|
+
'secret_index': [secret.start, secret.end],
|
|
86
|
+
'redaction_token': secret.redaction_token,
|
|
87
|
+
})
|
|
88
|
+
return secrets
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def scan_secrets(file_path=None, directory_path=None, recursive=False, data=None,
|
|
92
|
+
save_scan_result=None, scan_result_path=None, custom_pattern=None):
|
|
93
|
+
_validate_data_path(file_path=file_path, directory_path=directory_path, data=data)
|
|
94
|
+
target_files = []
|
|
95
|
+
scan_results = {}
|
|
96
|
+
if directory_path:
|
|
97
|
+
directory_path = os.path.abspath(directory_path)
|
|
98
|
+
if recursive:
|
|
99
|
+
for root, _, files in os.walk(directory_path):
|
|
100
|
+
target_files.extend(os.path.join(root, file) for file in files)
|
|
101
|
+
else:
|
|
102
|
+
for file in os.listdir(directory_path):
|
|
103
|
+
file = os.path.join(directory_path, file)
|
|
104
|
+
if os.path.isfile(file):
|
|
105
|
+
target_files.append(file)
|
|
106
|
+
if file_path:
|
|
107
|
+
file_path = os.path.abspath(file_path)
|
|
108
|
+
target_files.append(file_path)
|
|
109
|
+
|
|
110
|
+
if data:
|
|
111
|
+
secrets = _scan_secrets_for_string(data, custom_pattern)
|
|
112
|
+
if secrets:
|
|
113
|
+
scan_results['raw_data'] = secrets
|
|
114
|
+
elif target_files:
|
|
115
|
+
for target_file in target_files:
|
|
116
|
+
logger.debug('start scanning secrets for %s', target_file)
|
|
117
|
+
with open(target_file) as f:
|
|
118
|
+
data = f.read()
|
|
119
|
+
if not data:
|
|
120
|
+
continue
|
|
121
|
+
secrets = _scan_secrets_for_string(data, custom_pattern)
|
|
122
|
+
logger.debug('%d secrets found for %s', len(secrets), target_file)
|
|
123
|
+
if secrets:
|
|
124
|
+
scan_results[target_file] = secrets
|
|
125
|
+
|
|
126
|
+
if scan_result_path:
|
|
127
|
+
save_scan_result = True
|
|
128
|
+
if not save_scan_result:
|
|
129
|
+
return {
|
|
130
|
+
'secrets_detected': bool(scan_results),
|
|
131
|
+
'scan_results': scan_results
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if not scan_results:
|
|
135
|
+
return {'secrets_detected': False, 'scan_result_path': None}
|
|
136
|
+
|
|
137
|
+
if not scan_result_path:
|
|
138
|
+
from azdev.utilities.config import get_azdev_config_dir
|
|
139
|
+
from datetime import datetime
|
|
140
|
+
file_folder = os.path.join(get_azdev_config_dir(), 'scan_results')
|
|
141
|
+
if not os.path.exists(file_folder):
|
|
142
|
+
os.mkdir(file_folder, 0o755)
|
|
143
|
+
file_name = file_path or directory_path or datetime.now().strftime('%Y%m%d%H%M%S')
|
|
144
|
+
result_file_name = 'scan_result_' + file_name.replace('.', '_') + '.json'
|
|
145
|
+
scan_result_path = os.path.join(file_folder, result_file_name)
|
|
146
|
+
|
|
147
|
+
with open(scan_result_path, 'w') as f:
|
|
148
|
+
json.dump(scan_results, f)
|
|
149
|
+
logger.debug('store scanning results in %s', scan_result_path)
|
|
150
|
+
return {'secrets_detected': True, 'scan_result_path': os.path.abspath(scan_result_path)}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _get_scan_results_from_saved_file(saved_scan_result_path,
|
|
154
|
+
file_path=None, directory_path=None, recursive=False, data=None):
|
|
155
|
+
scan_results = {}
|
|
156
|
+
if not os.path.isfile(saved_scan_result_path):
|
|
157
|
+
raise ValueError(f'invalid saved scan result path:{saved_scan_result_path}')
|
|
158
|
+
with open(saved_scan_result_path) as f:
|
|
159
|
+
saved_scan_results = json.load(f)
|
|
160
|
+
# filter saved scan results to keep those related with specified file(s)
|
|
161
|
+
_validate_data_path(file_path=file_path, directory_path=directory_path, data=data)
|
|
162
|
+
if file_path:
|
|
163
|
+
file_path = os.path.abspath(file_path)
|
|
164
|
+
if file_path in saved_scan_results:
|
|
165
|
+
scan_results[file_path] = saved_scan_results[file_path]
|
|
166
|
+
elif directory_path:
|
|
167
|
+
if recursive:
|
|
168
|
+
for root, _, files in os.walk(directory_path):
|
|
169
|
+
for file in files:
|
|
170
|
+
file_full = os.path.join(root, file)
|
|
171
|
+
if file_full in saved_scan_results:
|
|
172
|
+
scan_results[file_full] = saved_scan_results[file_full]
|
|
173
|
+
else:
|
|
174
|
+
for file in os.listdir(directory_path):
|
|
175
|
+
file_full = os.path.join(directory_path, file)
|
|
176
|
+
if file_full in saved_scan_results:
|
|
177
|
+
scan_results[file_full] = saved_scan_results[file_full]
|
|
178
|
+
else:
|
|
179
|
+
scan_results['raw_data'] = saved_scan_results['raw_data']
|
|
180
|
+
|
|
181
|
+
return scan_results
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _mask_secret_for_string(data, secret, redaction_type=None):
|
|
185
|
+
if redaction_type == 'FIXED_VALUE':
|
|
186
|
+
data = data.replace(secret['secret_value'], '***')
|
|
187
|
+
elif redaction_type == 'FIXED_LENGTH':
|
|
188
|
+
data = data.replace(secret['secret_value'], '*' * len(secret['secret_value']))
|
|
189
|
+
elif redaction_type == 'SECRET_NAME':
|
|
190
|
+
data = data.replace(secret['secret_value'], secret['secret_name'])
|
|
191
|
+
else:
|
|
192
|
+
data = data.replace(secret['secret_value'], secret['redaction_token'])
|
|
193
|
+
return data
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def mask_secrets(file_path=None, directory_path=None, recursive=False, data=None,
|
|
197
|
+
save_scan_result=None, scan_result_path=None, custom_pattern=None,
|
|
198
|
+
saved_scan_result_path=None, redaction_type='FIXED_VALUE', yes=None):
|
|
199
|
+
scan_results = {}
|
|
200
|
+
if saved_scan_result_path:
|
|
201
|
+
scan_results = _get_scan_results_from_saved_file(saved_scan_result_path, file_path=file_path,
|
|
202
|
+
directory_path=directory_path, recursive=recursive, data=data)
|
|
203
|
+
else:
|
|
204
|
+
scan_response = scan_secrets(file_path=file_path, directory_path=directory_path, recursive=recursive, data=data,
|
|
205
|
+
save_scan_result=save_scan_result, scan_result_path=scan_result_path,
|
|
206
|
+
custom_pattern=custom_pattern)
|
|
207
|
+
if save_scan_result and scan_response['scan_result_path']:
|
|
208
|
+
with open(scan_response['scan_result_path']) as f:
|
|
209
|
+
scan_results = json.load(f)
|
|
210
|
+
elif not save_scan_result:
|
|
211
|
+
scan_results = scan_response['scan_results']
|
|
212
|
+
|
|
213
|
+
mask_result = {
|
|
214
|
+
'mask': False,
|
|
215
|
+
'data': data,
|
|
216
|
+
'file_path': file_path,
|
|
217
|
+
'directory_path': directory_path,
|
|
218
|
+
'recursive': recursive
|
|
219
|
+
}
|
|
220
|
+
if not scan_results:
|
|
221
|
+
logger.warning('No secrets detected, finish directly.')
|
|
222
|
+
return mask_result
|
|
223
|
+
for scan_file_path, secrets in scan_results.items():
|
|
224
|
+
logger.warning('Will mask %d secrets for %s', len(secrets), scan_file_path)
|
|
225
|
+
if not yes:
|
|
226
|
+
from knack.prompting import prompt_y_n
|
|
227
|
+
if not prompt_y_n(f'Do you want to continue with redaction type {redaction_type}?'):
|
|
228
|
+
return mask_result
|
|
229
|
+
|
|
230
|
+
if 'raw_data' in scan_results:
|
|
231
|
+
for secret in scan_results['raw_data']:
|
|
232
|
+
data = _mask_secret_for_string(data, secret, redaction_type)
|
|
233
|
+
mask_result['mask'] = True
|
|
234
|
+
mask_result['data'] = data
|
|
235
|
+
return mask_result
|
|
236
|
+
|
|
237
|
+
for scan_file_path, secrets in scan_results.items():
|
|
238
|
+
with open(scan_file_path, 'r') as f:
|
|
239
|
+
content = f.read()
|
|
240
|
+
if not content:
|
|
241
|
+
continue
|
|
242
|
+
for secret in secrets:
|
|
243
|
+
content = _mask_secret_for_string(content, secret, redaction_type)
|
|
244
|
+
with open(scan_file_path, 'w') as f:
|
|
245
|
+
f.write(content)
|
|
246
|
+
mask_result['mask'] = True
|
|
247
|
+
return mask_result
|
|
@@ -100,6 +100,35 @@ def load_arguments(self, _):
|
|
|
100
100
|
'Defaults to "high".')
|
|
101
101
|
# endregion
|
|
102
102
|
|
|
103
|
+
# region scan & mask
|
|
104
|
+
for scope in ['scan', 'mask']:
|
|
105
|
+
with ArgumentsContext(self, scope) as c:
|
|
106
|
+
c.argument('file_path', options_list=['--file-path', '-f'],
|
|
107
|
+
help='Path of the file you want to scan secrets for')
|
|
108
|
+
c.argument('directory_path', options_list=['--directory-path', '-d'],
|
|
109
|
+
help='Path of the folder you want to scan secrets for')
|
|
110
|
+
c.argument('recursive', options_list=['--recursive', '-r'],
|
|
111
|
+
help='Scan the directory recursively')
|
|
112
|
+
c.argument('data', help='Raw string you want to scan secrets for')
|
|
113
|
+
c.argument('save_scan_result', options_list=['--save-scan-result', '--save'], type=bool,
|
|
114
|
+
help='Whether to save scan result to file or not')
|
|
115
|
+
c.argument('scan_result_path', options_list=['--scan-result-path', '--result'],
|
|
116
|
+
help='Path for the file you want to save the result in. '
|
|
117
|
+
'If specified, --save-scan-result will be True anyway. '
|
|
118
|
+
'If not speficied but set --save-scan-result to True, '
|
|
119
|
+
'the file will be saved as `scan_result_xxx.json` in your `.azdev` directory ')
|
|
120
|
+
c.argument('custom_pattern',
|
|
121
|
+
help='Additional patterns you want to apply or built-in patterns you want to exclude '
|
|
122
|
+
'for scanning. Can be json string or path to the json file.')
|
|
123
|
+
|
|
124
|
+
with ArgumentsContext(self, 'mask') as c:
|
|
125
|
+
c.argument('yes', options_list=['--yes', '-y'], action='store_true', help='Answer "yes" to all prompts.')
|
|
126
|
+
c.argument('redaction_type', options_list=['--redaction-type', '--type'],
|
|
127
|
+
choices=['FIXED_VALUE', 'FIXED_LENGTH', 'SECRET_NAME', 'CUSTOM'])
|
|
128
|
+
c.argument('saved_scan_result_path', options_list=['--saved-scan-result-path', '--saved-result'],
|
|
129
|
+
help='Path of the file you saved the scan result in')
|
|
130
|
+
# endregion
|
|
131
|
+
|
|
103
132
|
# region statistics
|
|
104
133
|
with ArgumentsContext(self, 'statistics') as c:
|
|
105
134
|
c.argument('include_whl_extensions',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: azdev
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.74
|
|
4
4
|
Summary: Microsoft Azure CLI Developer Tools
|
|
5
5
|
Home-page: https://github.com/Azure/azure-cli-dev-tools
|
|
6
6
|
Author: Microsoft Corporation
|
|
@@ -40,6 +40,7 @@ Requires-Dist: azure-cli-diff-tool~=0.0.6
|
|
|
40
40
|
Requires-Dist: packaging
|
|
41
41
|
Requires-Dist: tqdm
|
|
42
42
|
Requires-Dist: wheel==0.30.0
|
|
43
|
+
Requires-Dist: microsoft-security-utilities-secret-masker
|
|
43
44
|
|
|
44
45
|
Microsoft Azure CLI Dev Tools (azdev)
|
|
45
46
|
=====================================
|
|
@@ -145,6 +146,15 @@ License
|
|
|
145
146
|
|
|
146
147
|
Release History
|
|
147
148
|
===============
|
|
149
|
+
0.1.74
|
|
150
|
+
++++++
|
|
151
|
+
* `azdev scan/mask`: New commands for scanning and masking secrets for files or string
|
|
152
|
+
|
|
153
|
+
0.1.73
|
|
154
|
+
++++++
|
|
155
|
+
* `azdev command-change meta-export`: Add `has_completer` to denote whether completer is configed in arg
|
|
156
|
+
* `azdev command-change meta-export`: Extracting arg help and example for loaded HelpFiles
|
|
157
|
+
|
|
148
158
|
0.1.72
|
|
149
159
|
++++++
|
|
150
160
|
* Bump `pylint` to 3
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|