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