1
1
// Copyright (c) Microsoft Corporation.
2
2
// Licensed under the MIT License.
3
3
using System ;
4
+ using System . Collections . Concurrent ;
4
5
using System . Collections . Generic ;
6
+ using System . Globalization ;
5
7
using System . Text . RegularExpressions ;
8
+ using System . Threading ;
9
+
6
10
using Microsoft . Security . Utilities ;
7
11
8
12
namespace Agent . Sdk . SecretMasking ;
9
13
10
14
public sealed class OssSecretMasker : IRawSecretMasker
11
15
{
12
16
private SecretMasker _secretMasker ;
17
+ private Telemetry _telemetry ;
13
18
14
- public OssSecretMasker ( ) : this ( Array . Empty < RegexPattern > ( ) )
15
- {
16
- }
17
-
18
- public OssSecretMasker ( IEnumerable < RegexPattern > patterns )
19
+ public OssSecretMasker ( IEnumerable < RegexPattern > patterns = null )
19
20
{
20
- _secretMasker = new SecretMasker ( patterns , generateCorrelatingIds : true ) ;
21
- _secretMasker . DefaultRegexRedactionToken = "***" ;
21
+ _secretMasker = new SecretMasker ( patterns ,
22
+ generateCorrelatingIds : true ,
23
+ defaultRegexRedactionToken : "***" ) ;
22
24
}
23
25
24
-
25
26
/// <summary>
26
27
/// This property allows to set the minimum length of a secret for masking
27
28
/// </summary>
@@ -31,9 +32,6 @@ public int MinSecretLength
31
32
set => _secretMasker . MinimumSecretLength = value ;
32
33
}
33
34
34
- /// <summary>
35
- /// This implementation assumes no more than one thread is adding regexes, values, or encoders at any given time.
36
- /// </summary>
37
35
public void AddRegex ( string pattern )
38
36
{
39
37
// NOTE: This code path is used for regexes sent to the agent via
@@ -52,95 +50,189 @@ public void AddRegex(string pattern)
52
50
_secretMasker . AddRegex ( regexPattern ) ;
53
51
}
54
52
55
- /// <summary>
56
- /// This implementation assumes no more than one thread is adding regexes, values, or encoders at any given time.
57
- /// </summary>
58
53
public void AddValue ( string test )
59
54
{
60
55
_secretMasker . AddValue ( test ) ;
61
56
}
62
57
63
- /// <summary>
64
- /// This implementation assumes no more than one thread is adding regexes, values, or encoders at any given time.
65
- /// </summary>
66
58
public void AddValueEncoder ( Func < string , string > encoder )
67
59
{
68
- _secretMasker . AddLiteralEncoder ( x => encoder ( x ) ) ;
60
+ _secretMasker . AddLiteralEncoder ( x => encoder ( x ) ) ;
69
61
}
70
62
71
63
public void Dispose ( )
72
64
{
73
65
_secretMasker ? . Dispose ( ) ;
74
66
_secretMasker = null ;
67
+ _telemetry = null ;
75
68
}
76
69
77
70
public string MaskSecrets ( string input )
78
71
{
79
- return _secretMasker . MaskSecrets ( input ) ;
72
+ _secretMasker . SyncObject . EnterReadLock ( ) ;
73
+ try
74
+ {
75
+ _telemetry ? . ProcessInput ( input ) ;
76
+ return _secretMasker . MaskSecrets ( input , _telemetry ? . ProcessDetection ) ;
77
+ }
78
+ finally
79
+ {
80
+ _secretMasker . SyncObject . ExitReadLock ( ) ;
81
+ }
80
82
}
81
83
82
- /// <summary>
83
- /// Removes secrets from the dictionary shorter than the MinSecretLength property.
84
- /// This implementation assumes no more than one thread is adding regexes, values, or encoders at any given time.
85
- /// </summary>
86
- public void RemoveShortSecretsFromDictionary ( )
84
+ public void StartTelemetry ( int maxUniqueCorrelatingIds )
87
85
{
88
- var filteredValueSecrets = new HashSet < SecretLiteral > ( ) ;
89
- var filteredRegexSecrets = new HashSet < RegexPattern > ( ) ;
90
-
86
+ _secretMasker . SyncObject . EnterWriteLock ( ) ;
91
87
try
92
88
{
93
- _secretMasker . SyncObject . EnterReadLock ( ) ;
89
+ _telemetry ??= new Telemetry ( maxUniqueCorrelatingIds ) ;
90
+ }
91
+ finally
92
+ {
93
+ _secretMasker . SyncObject . ExitWriteLock ( ) ;
94
+ }
95
+ }
94
96
95
- foreach ( var secret in _secretMasker . EncodedSecretLiterals )
96
- {
97
- if ( secret . Value . Length < MinSecretLength )
98
- {
99
- filteredValueSecrets . Add ( secret ) ;
100
- }
101
- }
97
+ public void StopAndPublishTelemetry ( PublishSecretMaskerTelemetryAction publishAction , int maxCorrelatingIdsPerEvent )
98
+ {
99
+ Telemetry telemetry ;
102
100
103
- foreach ( var secret in _secretMasker . RegexPatterns )
104
- {
105
- if ( secret . Pattern . Length < MinSecretLength )
106
- {
107
- filteredRegexSecrets . Add ( secret ) ;
108
- }
109
- }
101
+ _secretMasker . SyncObject . EnterWriteLock ( ) ;
102
+ try
103
+ {
104
+ telemetry = _telemetry ;
105
+ _telemetry = null ;
110
106
}
111
107
finally
112
108
{
113
- if ( _secretMasker . SyncObject . IsReadLockHeld )
114
- {
115
- _secretMasker . SyncObject . ExitReadLock ( ) ;
116
- }
109
+ _secretMasker . SyncObject . ExitWriteLock ( ) ;
117
110
}
118
111
119
- try
112
+ telemetry ? . Publish ( publishAction , _secretMasker . ElapsedMaskingTime , maxCorrelatingIdsPerEvent ) ;
113
+ }
114
+
115
+ private sealed class Telemetry
116
+ {
117
+ // NOTE: Telemetry does not fit into the reader-writer lock model of the
118
+ // SecretMasker API because we *write* telemetry during *read*
119
+ // operations. We therefore use separate interlocked operations and a
120
+ // concurrent dictionary when writing to telemetry.
121
+
122
+ // Key=CrossCompanyCorrelatingId (C3ID), Value=Rule Moniker C3ID is a
123
+ // non-reversible seeded hash and only available when detection is made
124
+ // by a high-confidence rule that matches secrets with high entropy.
125
+ private readonly ConcurrentDictionary < string , string > _correlationData ;
126
+ private readonly int _maxUniqueCorrelatingIds ;
127
+ private long _charsScanned ;
128
+ private long _stringsScanned ;
129
+ private long _totalDetections ;
130
+
131
+ public Telemetry ( int maxDetections )
120
132
{
121
- _secretMasker . SyncObject . EnterWriteLock ( ) ;
133
+ _correlationData = new ConcurrentDictionary < string , string > ( ) ;
134
+ _maxUniqueCorrelatingIds = maxDetections ;
135
+ ProcessDetection = ProcessDetectionImplementation ;
136
+ }
122
137
123
- foreach ( var secret in filteredValueSecrets )
124
- {
125
- _secretMasker . EncodedSecretLiterals . Remove ( secret ) ;
126
- }
138
+ public void ProcessInput ( string input )
139
+ {
140
+ Interlocked . Add ( ref _charsScanned , input . Length ) ;
141
+ Interlocked . Increment ( ref _stringsScanned ) ;
142
+ }
127
143
128
- foreach ( var secret in filteredRegexSecrets )
129
- {
130
- _secretMasker . RegexPatterns . Remove ( secret ) ;
131
- }
144
+ public Action < Detection > ProcessDetection { get ; }
132
145
133
- foreach ( var secret in filteredValueSecrets )
146
+ private void ProcessDetectionImplementation ( Detection detection )
147
+ {
148
+ Interlocked . Increment ( ref _totalDetections ) ;
149
+
150
+ // NOTE: We cannot prevent the concurrent dictionary from exceeding
151
+ // the maximum detection count when multiple threads add detections
152
+ // in parallel. The condition here is therefore a best effort to
153
+ // constrain the memory consumed by excess detections that will not
154
+ // be published. Furthermore, it is deliberate that we use <=
155
+ // instead of < here as it allows us to detect the case where the
156
+ // maximum number of events have been exceeded without adding any
157
+ // additional state.
158
+ if ( _correlationData . Count <= _maxUniqueCorrelatingIds &&
159
+ detection . CrossCompanyCorrelatingId != null )
134
160
{
135
- _secretMasker . ExplicitlyAddedSecretLiterals . Remove ( secret ) ;
161
+ _correlationData . TryAdd ( detection . CrossCompanyCorrelatingId , detection . Moniker ) ;
136
162
}
137
163
}
138
- finally
164
+
165
+ public void Publish ( PublishSecretMaskerTelemetryAction publishAction , TimeSpan elapsedMaskingTime , int maxCorrelatingIdsPerEvent )
139
166
{
140
- if ( _secretMasker . SyncObject . IsWriteLockHeld )
167
+ Dictionary < string , string > correlationData = null ;
168
+ int uniqueCorrelatingIds = 0 ;
169
+ bool correlationDataIsIncomplete = false ;
170
+
171
+ // Publish 'SecretMaskerCorrelation' events mapping unique C3IDs to
172
+ // rule moniker. No more than 'maxCorrelatingIdsPerEvent' are
173
+ // published in a single event.
174
+ foreach ( var pair in _correlationData )
141
175
{
142
- _secretMasker . SyncObject . ExitWriteLock ( ) ;
176
+ if ( uniqueCorrelatingIds >= _maxUniqueCorrelatingIds )
177
+ {
178
+ correlationDataIsIncomplete = true ;
179
+ break ;
180
+ }
181
+
182
+ correlationData ??= new Dictionary < string , string > ( maxCorrelatingIdsPerEvent ) ;
183
+ correlationData . Add ( pair . Key , pair . Value ) ;
184
+ uniqueCorrelatingIds ++ ;
185
+
186
+ if ( correlationData . Count >= maxCorrelatingIdsPerEvent )
187
+ {
188
+ publishAction ( "SecretMaskerCorrelation" , correlationData ) ;
189
+ correlationData = null ;
190
+ }
143
191
}
192
+
193
+ if ( correlationData != null )
194
+ {
195
+ publishAction ( "SecretMaskerCorrelation" , correlationData ) ;
196
+ correlationData = null ;
197
+ }
198
+
199
+ // Send overall information in a 'SecretMasker' event.
200
+ var overallData = new Dictionary < string , string > {
201
+ // The version of Microsoft.Security.Utilities.Core used.
202
+ { "Version" , SecretMasker . Version . ToString ( ) } ,
203
+
204
+ // The total number number of characters scanned by the secret masker.
205
+ { "CharsScanned" , _charsScanned . ToString ( CultureInfo . InvariantCulture ) } ,
206
+
207
+ // The total number of strings scanned by the secret masker.
208
+ { "StringsScanned" , _stringsScanned . ToString ( CultureInfo . InvariantCulture ) } ,
209
+
210
+ // The total number of detections made by the secret masker.
211
+ // This includes duplicate detections and detections without
212
+ // correlating IDs such as those made by literal values.
213
+ { "TotalDetections" , _totalDetections . ToString ( CultureInfo . InvariantCulture ) } ,
214
+
215
+ // The total amount of time spent masking secrets.
216
+ { "ElapsedMaskingTimeInMilliseconds" , elapsedMaskingTime . TotalMilliseconds . ToString ( CultureInfo . InvariantCulture ) } ,
217
+
218
+ // Whether the 'maxUniqueCorrelatingIds' limit was exceeded and
219
+ // therefore the 'SecretMaskerDetectionCorrelation' events does
220
+ // not contain all unique correlating IDs detected.
221
+ { "CorrelationDataIsIncomplete" , correlationDataIsIncomplete . ToString ( CultureInfo . InvariantCulture ) } ,
222
+
223
+ // The total number of unique correlating IDs reported in
224
+ // 'SecretMaskerCorrelation' events.
225
+ //
226
+ // NOTE: This may be less than the total number of unique
227
+ // correlating IDs if the maximum was exceeded. See above.
228
+ { "UniqueCorrelatingIds" , uniqueCorrelatingIds . ToString ( CultureInfo . InvariantCulture ) } ,
229
+ } ;
230
+
231
+ publishAction ( "SecretMasker" , overallData ) ;
144
232
}
145
233
}
234
+
235
+ // This is a no-op for the OSS SecretMasker because it respects
236
+ // MinimumSecretLength immediately without requiring an extra API call.
237
+ void IRawSecretMasker . RemoveShortSecretsFromDictionary ( ) { }
146
238
}
0 commit comments