azdev 0.1.73__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.
Files changed (102) hide show
  1. {azdev-0.1.73 → azdev-0.1.74}/HISTORY.rst +4 -0
  2. {azdev-0.1.73/azdev.egg-info → azdev-0.1.74}/PKG-INFO +6 -1
  3. {azdev-0.1.73 → azdev-0.1.74}/azdev/__init__.py +1 -1
  4. {azdev-0.1.73 → azdev-0.1.74}/azdev/commands.py +5 -0
  5. {azdev-0.1.73 → azdev-0.1.74}/azdev/help.py +46 -0
  6. azdev-0.1.74/azdev/operations/secret.py +247 -0
  7. {azdev-0.1.73 → azdev-0.1.74}/azdev/params.py +29 -0
  8. {azdev-0.1.73 → azdev-0.1.74/azdev.egg-info}/PKG-INFO +6 -1
  9. {azdev-0.1.73 → azdev-0.1.74}/azdev.egg-info/SOURCES.txt +1 -0
  10. {azdev-0.1.73 → azdev-0.1.74}/azdev.egg-info/requires.txt +1 -0
  11. {azdev-0.1.73 → azdev-0.1.74}/setup.py +2 -1
  12. {azdev-0.1.73 → azdev-0.1.74}/LICENSE +0 -0
  13. {azdev-0.1.73 → azdev-0.1.74}/MANIFEST.in +0 -0
  14. {azdev-0.1.73 → azdev-0.1.74}/README.md +0 -0
  15. {azdev-0.1.73 → azdev-0.1.74}/README.rst +0 -0
  16. {azdev-0.1.73 → azdev-0.1.74}/azdev/__main__.py +0 -0
  17. {azdev-0.1.73 → azdev-0.1.74}/azdev/completer.py +0 -0
  18. {azdev-0.1.73 → azdev-0.1.74}/azdev/config/__init__.py +0 -0
  19. {azdev-0.1.73 → azdev-0.1.74}/azdev/config/cli.flake8 +0 -0
  20. {azdev-0.1.73 → azdev-0.1.74}/azdev/config/cli_pylintrc +0 -0
  21. {azdev-0.1.73 → azdev-0.1.74}/azdev/config/ext.flake8 +0 -0
  22. {azdev-0.1.73 → azdev-0.1.74}/azdev/config/ext_pylintrc +0 -0
  23. {azdev-0.1.73 → azdev-0.1.74}/azdev/mod_templates/HISTORY.rst +0 -0
  24. {azdev-0.1.73 → azdev-0.1.74}/azdev/mod_templates/README.rst +0 -0
  25. {azdev-0.1.73 → azdev-0.1.74}/azdev/mod_templates/_client_factory.py +0 -0
  26. {azdev-0.1.73 → azdev-0.1.74}/azdev/mod_templates/_help.py +0 -0
  27. {azdev-0.1.73 → azdev-0.1.74}/azdev/mod_templates/_params.py +0 -0
  28. {azdev-0.1.73 → azdev-0.1.74}/azdev/mod_templates/_validators.py +0 -0
  29. {azdev-0.1.73 → azdev-0.1.74}/azdev/mod_templates/azext_metadata.json +0 -0
  30. {azdev-0.1.73 → azdev-0.1.74}/azdev/mod_templates/blank__init__.py +0 -0
  31. {azdev-0.1.73 → azdev-0.1.74}/azdev/mod_templates/commands.py +0 -0
  32. {azdev-0.1.73 → azdev-0.1.74}/azdev/mod_templates/custom.py +0 -0
  33. {azdev-0.1.73 → azdev-0.1.74}/azdev/mod_templates/module__init__.py +0 -0
  34. {azdev-0.1.73 → azdev-0.1.74}/azdev/mod_templates/pkg_declare__init__.py +0 -0
  35. {azdev-0.1.73 → azdev-0.1.74}/azdev/mod_templates/setup.cfg +0 -0
  36. {azdev-0.1.73 → azdev-0.1.74}/azdev/mod_templates/setup.py +0 -0
  37. {azdev-0.1.73 → azdev-0.1.74}/azdev/mod_templates/test_service_scenario.py +0 -0
  38. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/__init__.py +0 -0
  39. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/cmdcov/__init__.py +0 -0
  40. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/cmdcov/_macros.j2 +0 -0
  41. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/cmdcov/cmdcov.py +0 -0
  42. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/cmdcov/component.css +0 -0
  43. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/cmdcov/component.js +0 -0
  44. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/cmdcov/favicon.ico +0 -0
  45. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/cmdcov/index.j2 +0 -0
  46. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/cmdcov/index2.j2 +0 -0
  47. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/cmdcov/module.j2 +0 -0
  48. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/code_gen.py +0 -0
  49. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/command_change/__init__.py +0 -0
  50. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/command_change/custom.py +0 -0
  51. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/command_change/util.py +0 -0
  52. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/constant.py +0 -0
  53. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/extensions/__init__.py +0 -0
  54. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/extensions/util.py +0 -0
  55. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/extensions/version_upgrade.py +0 -0
  56. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/help/__init__.py +0 -0
  57. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/help/refdoc/__init__.py +0 -0
  58. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/help/refdoc/conf.py +0 -0
  59. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/legal.py +0 -0
  60. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/linter/__init__.py +0 -0
  61. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/linter/linter.py +0 -0
  62. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/linter/pylint_checkers/__init__.py +0 -0
  63. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/linter/pylint_checkers/show_command.py +0 -0
  64. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/linter/rule_decorators.py +0 -0
  65. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/linter/rules/__init__.py +0 -0
  66. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/linter/rules/ci_exclusions.yml +0 -0
  67. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/linter/rules/command_coverage_rules.py +0 -0
  68. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/linter/rules/command_group_rules.py +0 -0
  69. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/linter/rules/command_rules.py +0 -0
  70. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/linter/rules/help_rules.py +0 -0
  71. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/linter/rules/linter_exclusions.yml +0 -0
  72. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/linter/rules/parameter_rules.py +0 -0
  73. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/linter/util.py +0 -0
  74. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/performance.py +0 -0
  75. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/pypi.py +0 -0
  76. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/python_sdk.py +0 -0
  77. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/regex.py +0 -0
  78. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/resource.py +0 -0
  79. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/setup.py +0 -0
  80. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/statistics/__init__.py +0 -0
  81. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/statistics/util.py +0 -0
  82. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/style.py +0 -0
  83. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/testtool/__init__.py +0 -0
  84. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/testtool/incremental_strategy.py +0 -0
  85. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/testtool/profile_context.py +0 -0
  86. {azdev-0.1.73 → azdev-0.1.74}/azdev/operations/testtool/pytest_runner.py +0 -0
  87. {azdev-0.1.73 → azdev-0.1.74}/azdev/transformers.py +0 -0
  88. {azdev-0.1.73 → azdev-0.1.74}/azdev/utilities/__init__.py +0 -0
  89. {azdev-0.1.73 → azdev-0.1.74}/azdev/utilities/command.py +0 -0
  90. {azdev-0.1.73 → azdev-0.1.74}/azdev/utilities/config.py +0 -0
  91. {azdev-0.1.73 → azdev-0.1.74}/azdev/utilities/const.py +0 -0
  92. {azdev-0.1.73 → azdev-0.1.74}/azdev/utilities/display.py +0 -0
  93. {azdev-0.1.73 → azdev-0.1.74}/azdev/utilities/git_util.py +0 -0
  94. {azdev-0.1.73 → azdev-0.1.74}/azdev/utilities/path.py +0 -0
  95. {azdev-0.1.73 → azdev-0.1.74}/azdev/utilities/pypi.py +0 -0
  96. {azdev-0.1.73 → azdev-0.1.74}/azdev/utilities/testing.py +0 -0
  97. {azdev-0.1.73 → azdev-0.1.74}/azdev/utilities/tools.py +0 -0
  98. {azdev-0.1.73 → azdev-0.1.74}/azdev.egg-info/dependency_links.txt +0 -0
  99. {azdev-0.1.73 → azdev-0.1.74}/azdev.egg-info/entry_points.txt +0 -0
  100. {azdev-0.1.73 → azdev-0.1.74}/azdev.egg-info/top_level.txt +0 -0
  101. {azdev-0.1.73 → azdev-0.1.74}/pyproject.toml +0 -0
  102. {azdev-0.1.73 → azdev-0.1.74}/setup.cfg +0 -0
@@ -2,6 +2,10 @@
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
+
5
9
  0.1.73
6
10
  ++++++
7
11
  * `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.73
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,10 @@ 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
+
148
153
  0.1.73
149
154
  ++++++
150
155
  * `azdev command-change meta-export`: Add `has_completer` to denote whether completer is configed in arg
@@ -4,4 +4,4 @@
4
4
  # license information.
5
5
  # -----------------------------------------------------------------------------
6
6
 
7
- __VERSION__ = '0.1.73'
7
+ __VERSION__ = '0.1.74'
@@ -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
  """
@@ -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.73
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,10 @@ 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
+
148
153
  0.1.73
149
154
  ++++++
150
155
  * `azdev command-change meta-export`: Add `has_completer` to denote whether completer is configed in arg
@@ -48,6 +48,7 @@ azdev/operations/pypi.py
48
48
  azdev/operations/python_sdk.py
49
49
  azdev/operations/regex.py
50
50
  azdev/operations/resource.py
51
+ azdev/operations/secret.py
51
52
  azdev/operations/setup.py
52
53
  azdev/operations/style.py
53
54
  azdev/operations/cmdcov/__init__.py
@@ -18,3 +18,4 @@ azure-cli-diff-tool~=0.0.6
18
18
  packaging
19
19
  tqdm
20
20
  wheel==0.30.0
21
+ microsoft-security-utilities-secret-masker
@@ -85,7 +85,8 @@ setup(
85
85
  'azure-cli-diff-tool~=0.0.6',
86
86
  'packaging',
87
87
  'tqdm',
88
- 'wheel==0.30.0'
88
+ 'wheel==0.30.0',
89
+ 'microsoft-security-utilities-secret-masker'
89
90
  ],
90
91
  package_data={
91
92
  'azdev.config': ['*.*', 'cli_pylintrc', 'ext_pylintrc'],
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