azdev 0.1.73__tar.gz → 0.1.76__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.73 → azdev-0.1.76}/HISTORY.rst +12 -0
- {azdev-0.1.73/azdev.egg-info → azdev-0.1.76}/PKG-INFO +14 -1
- {azdev-0.1.73 → azdev-0.1.76}/azdev/__init__.py +1 -1
- {azdev-0.1.73 → azdev-0.1.76}/azdev/commands.py +5 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/help.py +49 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/extensions/version_upgrade.py +4 -0
- azdev-0.1.76/azdev/operations/secret.py +291 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/params.py +37 -0
- {azdev-0.1.73 → azdev-0.1.76/azdev.egg-info}/PKG-INFO +14 -1
- {azdev-0.1.73 → azdev-0.1.76}/azdev.egg-info/SOURCES.txt +1 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev.egg-info/requires.txt +1 -0
- {azdev-0.1.73 → azdev-0.1.76}/setup.py +2 -1
- {azdev-0.1.73 → azdev-0.1.76}/LICENSE +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/MANIFEST.in +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/README.md +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/README.rst +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/__main__.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/completer.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/config/__init__.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/config/cli.flake8 +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/config/cli_pylintrc +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/config/ext.flake8 +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/config/ext_pylintrc +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/mod_templates/HISTORY.rst +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/mod_templates/README.rst +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/mod_templates/_client_factory.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/mod_templates/_help.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/mod_templates/_params.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/mod_templates/_validators.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/mod_templates/azext_metadata.json +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/mod_templates/blank__init__.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/mod_templates/commands.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/mod_templates/custom.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/mod_templates/module__init__.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/mod_templates/pkg_declare__init__.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/mod_templates/setup.cfg +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/mod_templates/setup.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/mod_templates/test_service_scenario.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/__init__.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/cmdcov/__init__.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/cmdcov/_macros.j2 +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/cmdcov/cmdcov.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/cmdcov/component.css +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/cmdcov/component.js +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/cmdcov/favicon.ico +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/cmdcov/index.j2 +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/cmdcov/index2.j2 +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/cmdcov/module.j2 +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/code_gen.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/command_change/__init__.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/command_change/custom.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/command_change/util.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/constant.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/extensions/__init__.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/extensions/util.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/help/__init__.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/help/refdoc/__init__.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/help/refdoc/conf.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/legal.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/linter/__init__.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/linter/linter.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/linter/pylint_checkers/__init__.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/linter/pylint_checkers/show_command.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/linter/rule_decorators.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/linter/rules/__init__.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/linter/rules/ci_exclusions.yml +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/linter/rules/command_coverage_rules.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/linter/rules/command_group_rules.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/linter/rules/command_rules.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/linter/rules/help_rules.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/linter/rules/linter_exclusions.yml +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/linter/rules/parameter_rules.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/linter/util.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/performance.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/pypi.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/python_sdk.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/regex.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/resource.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/setup.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/statistics/__init__.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/statistics/util.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/style.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/testtool/__init__.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/testtool/incremental_strategy.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/testtool/profile_context.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/operations/testtool/pytest_runner.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/transformers.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/utilities/__init__.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/utilities/command.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/utilities/config.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/utilities/const.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/utilities/display.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/utilities/git_util.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/utilities/path.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/utilities/pypi.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/utilities/testing.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev/utilities/tools.py +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev.egg-info/dependency_links.txt +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev.egg-info/entry_points.txt +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/azdev.egg-info/top_level.txt +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/pyproject.toml +0 -0
- {azdev-0.1.73 → azdev-0.1.76}/setup.cfg +0 -0
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Release History
|
|
4
4
|
===============
|
|
5
|
+
0.1.76
|
|
6
|
+
++++++
|
|
7
|
+
* `azdev extension cal-next-version`: Fix preview to stable version case.
|
|
8
|
+
|
|
9
|
+
0.1.75
|
|
10
|
+
++++++
|
|
11
|
+
* `azdev scan/mask`: Add `--include-pattern` and `--exclude-pattern` to support filtering files within directory
|
|
12
|
+
|
|
13
|
+
0.1.74
|
|
14
|
+
++++++
|
|
15
|
+
* `azdev scan/mask`: New commands for scanning and masking secrets for files or string
|
|
16
|
+
|
|
5
17
|
0.1.73
|
|
6
18
|
++++++
|
|
7
19
|
* `azdev command-change meta-export`: Add `has_completer` to denote whether completer is configed in arg
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: azdev
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.76
|
|
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,18 @@ License
|
|
|
145
146
|
|
|
146
147
|
Release History
|
|
147
148
|
===============
|
|
149
|
+
0.1.76
|
|
150
|
+
++++++
|
|
151
|
+
* `azdev extension cal-next-version`: Fix preview to stable version case.
|
|
152
|
+
|
|
153
|
+
0.1.75
|
|
154
|
+
++++++
|
|
155
|
+
* `azdev scan/mask`: Add `--include-pattern` and `--exclude-pattern` to support filtering files within directory
|
|
156
|
+
|
|
157
|
+
0.1.74
|
|
158
|
+
++++++
|
|
159
|
+
* `azdev scan/mask`: New commands for scanning and masking secrets for files or string
|
|
160
|
+
|
|
148
161
|
0.1.73
|
|
149
162
|
++++++
|
|
150
163
|
* `azdev command-change meta-export`: Add `has_completer` to denote whether completer is configed in arg
|
|
@@ -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,54 @@ 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
|
+
- name: Scan secrets for all json files and yaml files within a directory
|
|
197
|
+
text: |
|
|
198
|
+
azdev scan --directory-path /path/to/my/folder --include-pattern *.yaml *.json
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
helps['mask'] = """
|
|
202
|
+
short-summary: Mask secrets for files or string
|
|
203
|
+
long-summary: |
|
|
204
|
+
Redaction type 'FIXED_VALUE' will mask all secrets with '***'.
|
|
205
|
+
Redaction type 'FIXED_LENGTH' will mask secrets with several '*'s which will keep the original secret length.
|
|
206
|
+
Redaction type 'SECRET_NAME' redaction type will mask secrets with their secret name (type).
|
|
207
|
+
Redaction type 'CUSTOM' will mask secrets with 'redaction_token' value you specify through saved scan result file.
|
|
208
|
+
Check built-in scanning rules at https://github.com/microsoft/security-utilities/blob/main/GeneratedRegexPatterns/PreciselyClassifiedSecurityKeys.json
|
|
209
|
+
"""
|
|
210
|
+
|
|
162
211
|
helps['statistics'] = """
|
|
163
212
|
short-summary: Commands for CLI modules statistics.
|
|
164
213
|
"""
|
|
@@ -123,6 +123,10 @@ class VersionUpgradeMod:
|
|
|
123
123
|
self.next_version.init_preview_version()
|
|
124
124
|
return
|
|
125
125
|
|
|
126
|
+
if self.next_version_pre_tag == VERSION_STABLE_TAG and self.is_preview:
|
|
127
|
+
# 2.0.0bN -> stable > 2.0.0
|
|
128
|
+
return
|
|
129
|
+
|
|
126
130
|
if self.next_version_segment_tag:
|
|
127
131
|
if self.next_version_segment_tag == VERSION_MAJOR_TAG:
|
|
128
132
|
self.next_version.major = self.version.major + 1
|
|
@@ -0,0 +1,291 @@
|
|
|
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, include_pattern=None, exclude_pattern=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
|
+
if not directory_path and include_pattern:
|
|
32
|
+
raise ValueError('--include-pattern need to be used together with --directory-path')
|
|
33
|
+
if not directory_path and exclude_pattern:
|
|
34
|
+
raise ValueError('--exclude-pattern need to be used together with --directory-path')
|
|
35
|
+
if include_pattern and exclude_pattern:
|
|
36
|
+
raise ValueError('--include-pattern and --exclude-pattern are mutually exclusive')
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _is_file_name_in_patterns(filename, patterns):
|
|
40
|
+
if not filename or not patterns:
|
|
41
|
+
return None
|
|
42
|
+
import fnmatch
|
|
43
|
+
for pattern in patterns:
|
|
44
|
+
if fnmatch.fnmatch(filename, pattern):
|
|
45
|
+
return True
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _check_file_include_and_exclude_pattern(filename, include_pattern=None, exclude_pattern=None):
|
|
50
|
+
file_satisfied = True
|
|
51
|
+
if include_pattern and not _is_file_name_in_patterns(filename, include_pattern):
|
|
52
|
+
file_satisfied = False
|
|
53
|
+
if exclude_pattern and _is_file_name_in_patterns(filename, exclude_pattern):
|
|
54
|
+
file_satisfied = False
|
|
55
|
+
return file_satisfied
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _get_files_from_directory(directory_path, recursive=None, include_pattern=None, exclude_pattern=None):
|
|
59
|
+
target_files = []
|
|
60
|
+
if recursive:
|
|
61
|
+
for root, _, files in os.walk(directory_path):
|
|
62
|
+
for file in files:
|
|
63
|
+
if _check_file_include_and_exclude_pattern(file,
|
|
64
|
+
include_pattern=include_pattern,
|
|
65
|
+
exclude_pattern=exclude_pattern):
|
|
66
|
+
target_files.append(os.path.join(root, file))
|
|
67
|
+
else:
|
|
68
|
+
for file in os.listdir(directory_path):
|
|
69
|
+
if _check_file_include_and_exclude_pattern(file,
|
|
70
|
+
include_pattern=include_pattern,
|
|
71
|
+
exclude_pattern=exclude_pattern):
|
|
72
|
+
file = os.path.join(directory_path, file)
|
|
73
|
+
if os.path.isfile(file):
|
|
74
|
+
target_files.append(file)
|
|
75
|
+
return target_files
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _load_built_in_regex_patterns():
|
|
79
|
+
return load_regex_patterns_from_json_file('PreciselyClassifiedSecurityKeys.json')
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _load_regex_patterns(custom_pattern=None):
|
|
83
|
+
built_in_regex_patterns = _load_built_in_regex_patterns()
|
|
84
|
+
|
|
85
|
+
if not custom_pattern:
|
|
86
|
+
return built_in_regex_patterns
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
if os.path.isfile(custom_pattern):
|
|
90
|
+
custom_pattern = json.load(custom_pattern)
|
|
91
|
+
else:
|
|
92
|
+
custom_pattern = json.loads(custom_pattern)
|
|
93
|
+
except JSONDecodeError as err:
|
|
94
|
+
raise ValueError(f'Custom pattern should be in valid json format, err:{err.msg}')
|
|
95
|
+
|
|
96
|
+
regex_patterns = []
|
|
97
|
+
if 'Include' in custom_pattern:
|
|
98
|
+
for pattern in custom_pattern['Include']:
|
|
99
|
+
if not pattern.get('Pattern', None):
|
|
100
|
+
raise ValueError(f'Invalid Custom Pattern: {pattern}, '
|
|
101
|
+
f'"Pattern" property is required for Include patterns')
|
|
102
|
+
regex_patterns.append(load_regex_pattern_from_json(pattern))
|
|
103
|
+
if "Exclude" in custom_pattern:
|
|
104
|
+
exclude_pattern_ids = []
|
|
105
|
+
for pattern in custom_pattern['Exclude']:
|
|
106
|
+
if not pattern.get('Id', None):
|
|
107
|
+
raise ValueError(f'Invalid Custom Pattern: {pattern}, "Id" property is required for Exclude patterns')
|
|
108
|
+
exclude_pattern_ids.append(pattern['Id'])
|
|
109
|
+
for pattern in built_in_regex_patterns:
|
|
110
|
+
if pattern.id in exclude_pattern_ids:
|
|
111
|
+
continue
|
|
112
|
+
regex_patterns.append(pattern)
|
|
113
|
+
else:
|
|
114
|
+
regex_patterns.extend(built_in_regex_patterns)
|
|
115
|
+
return regex_patterns
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _scan_secrets_for_string(data, custom_pattern=None):
|
|
119
|
+
if not data:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
regex_patterns = _load_regex_patterns(custom_pattern)
|
|
123
|
+
secret_masker = SecretMasker(regex_patterns)
|
|
124
|
+
detected_secrets = secret_masker.detect_secrets(data)
|
|
125
|
+
secrets = []
|
|
126
|
+
for secret in detected_secrets:
|
|
127
|
+
secrets.append({
|
|
128
|
+
'secret_name': secret.name,
|
|
129
|
+
'secret_value': data[secret.start:secret.end],
|
|
130
|
+
'secret_index': [secret.start, secret.end],
|
|
131
|
+
'redaction_token': secret.redaction_token,
|
|
132
|
+
})
|
|
133
|
+
return secrets
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def scan_secrets(file_path=None, directory_path=None, recursive=False,
|
|
137
|
+
include_pattern=None, exclude_pattern=None, data=None,
|
|
138
|
+
save_scan_result=None, scan_result_path=None, custom_pattern=None):
|
|
139
|
+
_validate_data_path(file_path=file_path, directory_path=directory_path,
|
|
140
|
+
include_pattern=include_pattern, exclude_pattern=exclude_pattern, data=data)
|
|
141
|
+
target_files = []
|
|
142
|
+
scan_results = {}
|
|
143
|
+
if directory_path:
|
|
144
|
+
directory_path = os.path.abspath(directory_path)
|
|
145
|
+
target_files = _get_files_from_directory(directory_path, recursive=recursive,
|
|
146
|
+
include_pattern=include_pattern, exclude_pattern=exclude_pattern)
|
|
147
|
+
if file_path:
|
|
148
|
+
file_path = os.path.abspath(file_path)
|
|
149
|
+
target_files.append(file_path)
|
|
150
|
+
|
|
151
|
+
if data:
|
|
152
|
+
secrets = _scan_secrets_for_string(data, custom_pattern)
|
|
153
|
+
if secrets:
|
|
154
|
+
scan_results['raw_data'] = secrets
|
|
155
|
+
elif target_files:
|
|
156
|
+
for target_file in target_files:
|
|
157
|
+
logger.debug('start scanning secrets for %s', target_file)
|
|
158
|
+
with open(target_file, encoding='utf8') as f:
|
|
159
|
+
data = f.read()
|
|
160
|
+
if not data:
|
|
161
|
+
continue
|
|
162
|
+
secrets = _scan_secrets_for_string(data, custom_pattern)
|
|
163
|
+
logger.debug('%d secrets found for %s', len(secrets), target_file)
|
|
164
|
+
if secrets:
|
|
165
|
+
scan_results[target_file] = secrets
|
|
166
|
+
|
|
167
|
+
if scan_result_path:
|
|
168
|
+
save_scan_result = True
|
|
169
|
+
if not save_scan_result:
|
|
170
|
+
return {
|
|
171
|
+
'secrets_detected': bool(scan_results),
|
|
172
|
+
'scan_results': scan_results
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if not scan_results:
|
|
176
|
+
return {'secrets_detected': False, 'scan_result_path': None}
|
|
177
|
+
|
|
178
|
+
if not scan_result_path:
|
|
179
|
+
from azdev.utilities.config import get_azdev_config_dir
|
|
180
|
+
from datetime import datetime
|
|
181
|
+
file_folder = os.path.join(get_azdev_config_dir(), 'scan_results')
|
|
182
|
+
if not os.path.exists(file_folder):
|
|
183
|
+
os.mkdir(file_folder, 0o755)
|
|
184
|
+
result_file_name = 'scan_result_' + datetime.now().strftime('%Y%m%d%H%M%S') + '.json'
|
|
185
|
+
scan_result_path = os.path.join(file_folder, result_file_name)
|
|
186
|
+
|
|
187
|
+
with open(scan_result_path, 'w', encoding='utf8') as f:
|
|
188
|
+
json.dump(scan_results, f)
|
|
189
|
+
logger.debug('store scanning results in %s', scan_result_path)
|
|
190
|
+
return {'secrets_detected': True, 'scan_result_path': os.path.abspath(scan_result_path)}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _get_scan_results_from_saved_file(saved_scan_result_path,
|
|
194
|
+
file_path=None, directory_path=None, recursive=False,
|
|
195
|
+
include_pattern=None, exclude_pattern=None, data=None):
|
|
196
|
+
scan_results = {}
|
|
197
|
+
if not os.path.isfile(saved_scan_result_path):
|
|
198
|
+
raise ValueError(f'invalid saved scan result path:{saved_scan_result_path}')
|
|
199
|
+
with open(saved_scan_result_path, encoding='utf8') as f:
|
|
200
|
+
saved_scan_results = json.load(f)
|
|
201
|
+
# filter saved scan results to keep those related with specified file(s)
|
|
202
|
+
_validate_data_path(file_path=file_path, directory_path=directory_path,
|
|
203
|
+
include_pattern=include_pattern, exclude_pattern=exclude_pattern, data=data)
|
|
204
|
+
if file_path:
|
|
205
|
+
file_path = os.path.abspath(file_path)
|
|
206
|
+
if file_path in saved_scan_results:
|
|
207
|
+
scan_results[file_path] = saved_scan_results[file_path]
|
|
208
|
+
elif directory_path:
|
|
209
|
+
directory_path = os.path.abspath(directory_path)
|
|
210
|
+
target_files = _get_files_from_directory(directory_path, recursive=recursive,
|
|
211
|
+
include_pattern=include_pattern, exclude_pattern=exclude_pattern)
|
|
212
|
+
for target_file in target_files:
|
|
213
|
+
if target_file in saved_scan_results:
|
|
214
|
+
scan_results[target_file] = saved_scan_results[target_file]
|
|
215
|
+
else:
|
|
216
|
+
scan_results['raw_data'] = saved_scan_results['raw_data']
|
|
217
|
+
|
|
218
|
+
return scan_results
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _mask_secret_for_string(data, secret, redaction_type=None):
|
|
222
|
+
if redaction_type == 'FIXED_VALUE':
|
|
223
|
+
data = data.replace(secret['secret_value'], '***')
|
|
224
|
+
elif redaction_type == 'FIXED_LENGTH':
|
|
225
|
+
data = data.replace(secret['secret_value'], '*' * len(secret['secret_value']))
|
|
226
|
+
elif redaction_type == 'SECRET_NAME':
|
|
227
|
+
data = data.replace(secret['secret_value'], secret['secret_name'])
|
|
228
|
+
else:
|
|
229
|
+
data = data.replace(secret['secret_value'], secret['redaction_token'])
|
|
230
|
+
return data
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def mask_secrets(file_path=None, directory_path=None, recursive=False,
|
|
234
|
+
include_pattern=None, exclude_pattern=None, data=None,
|
|
235
|
+
save_scan_result=None, scan_result_path=None, custom_pattern=None,
|
|
236
|
+
saved_scan_result_path=None, redaction_type='FIXED_VALUE', yes=None):
|
|
237
|
+
scan_results = {}
|
|
238
|
+
if saved_scan_result_path:
|
|
239
|
+
scan_results = _get_scan_results_from_saved_file(saved_scan_result_path,
|
|
240
|
+
file_path=file_path,
|
|
241
|
+
directory_path=directory_path,
|
|
242
|
+
recursive=recursive,
|
|
243
|
+
include_pattern=include_pattern,
|
|
244
|
+
exclude_pattern=exclude_pattern,
|
|
245
|
+
data=data)
|
|
246
|
+
else:
|
|
247
|
+
scan_response = scan_secrets(file_path=file_path, directory_path=directory_path, recursive=recursive,
|
|
248
|
+
include_pattern=include_pattern, exclude_pattern=exclude_pattern, data=data,
|
|
249
|
+
save_scan_result=save_scan_result, scan_result_path=scan_result_path,
|
|
250
|
+
custom_pattern=custom_pattern)
|
|
251
|
+
if save_scan_result and scan_response['scan_result_path']:
|
|
252
|
+
with open(scan_response['scan_result_path'], encoding='utf8') as f:
|
|
253
|
+
scan_results = json.load(f)
|
|
254
|
+
elif not save_scan_result:
|
|
255
|
+
scan_results = scan_response['scan_results']
|
|
256
|
+
|
|
257
|
+
mask_result = {
|
|
258
|
+
'mask': False,
|
|
259
|
+
'data': data,
|
|
260
|
+
'file_path': file_path,
|
|
261
|
+
'directory_path': directory_path,
|
|
262
|
+
'recursive': recursive
|
|
263
|
+
}
|
|
264
|
+
if not scan_results:
|
|
265
|
+
logger.warning('No secrets detected, finish directly.')
|
|
266
|
+
return mask_result
|
|
267
|
+
for scan_file_path, secrets in scan_results.items():
|
|
268
|
+
logger.warning('Will mask %d secrets for %s', len(secrets), scan_file_path)
|
|
269
|
+
if not yes:
|
|
270
|
+
from knack.prompting import prompt_y_n
|
|
271
|
+
if not prompt_y_n(f'Do you want to continue with redaction type {redaction_type}?'):
|
|
272
|
+
return mask_result
|
|
273
|
+
|
|
274
|
+
if 'raw_data' in scan_results:
|
|
275
|
+
for secret in scan_results['raw_data']:
|
|
276
|
+
data = _mask_secret_for_string(data, secret, redaction_type)
|
|
277
|
+
mask_result['mask'] = True
|
|
278
|
+
mask_result['data'] = data
|
|
279
|
+
return mask_result
|
|
280
|
+
|
|
281
|
+
for scan_file_path, secrets in scan_results.items():
|
|
282
|
+
with open(scan_file_path, 'r', encoding='utf8') as f:
|
|
283
|
+
content = f.read()
|
|
284
|
+
if not content:
|
|
285
|
+
continue
|
|
286
|
+
for secret in secrets:
|
|
287
|
+
content = _mask_secret_for_string(content, secret, redaction_type)
|
|
288
|
+
with open(scan_file_path, 'w', encoding='utf8') as f:
|
|
289
|
+
f.write(content)
|
|
290
|
+
mask_result['mask'] = True
|
|
291
|
+
return mask_result
|
|
@@ -100,6 +100,43 @@ 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('include_pattern', options_list=['--include-pattern', '--include'], nargs='*',
|
|
113
|
+
help="Space separated patterns used for files you want to include within the directory. "
|
|
114
|
+
"The supported patterns are '*', '?', '[seq]', and '[!seq]'. "
|
|
115
|
+
"For more information, please refer to https://docs.python.org/3/library/fnmatch.html")
|
|
116
|
+
c.argument('exclude_pattern', options_list=['--exclude-pattern', '--exclude'], nargs='*',
|
|
117
|
+
help="Space separated patterns used for files you want to exclude within the directory. "
|
|
118
|
+
"The supported patterns are '*', '?', '[seq]', and '[!seq]'. "
|
|
119
|
+
"For more information, please refer to https://docs.python.org/3/library/fnmatch.html")
|
|
120
|
+
c.argument('data', help='Raw string you want to scan secrets for')
|
|
121
|
+
c.argument('save_scan_result', options_list=['--save-scan-result', '--save'], action='store_true',
|
|
122
|
+
help='Whether to save scan result to file or not')
|
|
123
|
+
c.argument('scan_result_path', options_list=['--scan-result-path', '--result'],
|
|
124
|
+
help='Path for the file you want to save the result in. '
|
|
125
|
+
'If specified, --save-scan-result will be True anyway. '
|
|
126
|
+
'If not speficied but set --save-scan-result to True, '
|
|
127
|
+
'the file will be saved as `scan_result_YYYYmmddHHMMSS.json` in your `.azdev` directory ')
|
|
128
|
+
c.argument('custom_pattern',
|
|
129
|
+
help='Additional patterns you want to apply or built-in patterns you want to exclude '
|
|
130
|
+
'for scanning. Can be json string or path to the json file.')
|
|
131
|
+
|
|
132
|
+
with ArgumentsContext(self, 'mask') as c:
|
|
133
|
+
c.argument('yes', options_list=['--yes', '-y'], action='store_true', help='Answer "yes" to all prompts.')
|
|
134
|
+
c.argument('redaction_type', options_list=['--redaction-type', '--type'],
|
|
135
|
+
choices=['FIXED_VALUE', 'FIXED_LENGTH', 'SECRET_NAME', 'CUSTOM'])
|
|
136
|
+
c.argument('saved_scan_result_path', options_list=['--saved-scan-result-path', '--saved-result'],
|
|
137
|
+
help='Path of the file you saved the scan result in')
|
|
138
|
+
# endregion
|
|
139
|
+
|
|
103
140
|
# region statistics
|
|
104
141
|
with ArgumentsContext(self, 'statistics') as c:
|
|
105
142
|
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.76
|
|
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,18 @@ License
|
|
|
145
146
|
|
|
146
147
|
Release History
|
|
147
148
|
===============
|
|
149
|
+
0.1.76
|
|
150
|
+
++++++
|
|
151
|
+
* `azdev extension cal-next-version`: Fix preview to stable version case.
|
|
152
|
+
|
|
153
|
+
0.1.75
|
|
154
|
+
++++++
|
|
155
|
+
* `azdev scan/mask`: Add `--include-pattern` and `--exclude-pattern` to support filtering files within directory
|
|
156
|
+
|
|
157
|
+
0.1.74
|
|
158
|
+
++++++
|
|
159
|
+
* `azdev scan/mask`: New commands for scanning and masking secrets for files or string
|
|
160
|
+
|
|
148
161
|
0.1.73
|
|
149
162
|
++++++
|
|
150
163
|
* `azdev command-change meta-export`: Add `has_completer` to denote whether completer is configed in arg
|
|
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
|
|
File without changes
|