|
12 | 12 | using System.Text;
|
13 | 13 | using System.Text.RegularExpressions;
|
14 | 14 | using System.Threading;
|
| 15 | +using System.Management.Automation.Language; |
15 | 16 | using Microsoft.PowerShell.PSReadLine;
|
16 | 17 |
|
17 | 18 | namespace Microsoft.PowerShell
|
@@ -116,6 +117,20 @@ public class HistoryItem
|
116 | 117 | "password|asplaintext|token|apikey|secret",
|
117 | 118 | RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
118 | 119 |
|
| 120 | + private static readonly HashSet<string> s_SecretMgmtCommands = new(StringComparer.OrdinalIgnoreCase) |
| 121 | + { |
| 122 | + "Get-Secret", |
| 123 | + "Get-SecretInfo", |
| 124 | + "Get-SecretVault", |
| 125 | + "Register-SecretVault", |
| 126 | + "Remove-Secret", |
| 127 | + "Set-SecretInfo", |
| 128 | + "Set-SecretVaultDefault", |
| 129 | + "Test-SecretVault", |
| 130 | + "Unlock-SecretVault", |
| 131 | + "Unregister-SecretVault" |
| 132 | + }; |
| 133 | + |
119 | 134 | private void ClearSavedCurrentLine()
|
120 | 135 | {
|
121 | 136 | _savedCurrentLine.CommandLine = null;
|
@@ -474,11 +489,169 @@ void UpdateHistoryFromFile(IEnumerable<string> historyLines, bool fromDifferentS
|
474 | 489 | }
|
475 | 490 | }
|
476 | 491 |
|
| 492 | + private static bool IsOnLeftSideOfAnAssignment(Ast ast, out Ast rhs) |
| 493 | + { |
| 494 | + bool result = false; |
| 495 | + rhs = null; |
| 496 | + |
| 497 | + do |
| 498 | + { |
| 499 | + if (ast.Parent is AssignmentStatementAst assignment) |
| 500 | + { |
| 501 | + rhs = assignment.Right; |
| 502 | + result = ReferenceEquals(assignment.Left, ast); |
| 503 | + |
| 504 | + break; |
| 505 | + } |
| 506 | + |
| 507 | + ast = ast.Parent; |
| 508 | + } |
| 509 | + while (ast.Parent is not null); |
| 510 | + |
| 511 | + return result; |
| 512 | + } |
| 513 | + |
| 514 | + private static bool IsSecretMgmtCommand(StringConstantExpressionAst strConst, out CommandAst command) |
| 515 | + { |
| 516 | + bool result = false; |
| 517 | + command = strConst.Parent as CommandAst; |
| 518 | + |
| 519 | + if (command is not null) |
| 520 | + { |
| 521 | + result = ReferenceEquals(command.CommandElements[0], strConst) |
| 522 | + && s_SecretMgmtCommands.Contains(strConst.Value); |
| 523 | + } |
| 524 | + |
| 525 | + return result; |
| 526 | + } |
| 527 | + |
| 528 | + private static ExpressionAst GetArgumentForParameter(CommandParameterAst param) |
| 529 | + { |
| 530 | + if (param.Argument is not null) |
| 531 | + { |
| 532 | + return param.Argument; |
| 533 | + } |
| 534 | + |
| 535 | + var command = (CommandAst)param.Parent; |
| 536 | + int index = 1; |
| 537 | + for (; index < command.CommandElements.Count; index++) |
| 538 | + { |
| 539 | + if (ReferenceEquals(command.CommandElements[index], param)) |
| 540 | + { |
| 541 | + break; |
| 542 | + } |
| 543 | + } |
| 544 | + |
| 545 | + int argIndex = index + 1; |
| 546 | + if (argIndex < command.CommandElements.Count |
| 547 | + && command.CommandElements[argIndex] is ExpressionAst arg) |
| 548 | + { |
| 549 | + return arg; |
| 550 | + } |
| 551 | + |
| 552 | + return null; |
| 553 | + } |
| 554 | + |
477 | 555 | public static AddToHistoryOption GetDefaultAddToHistoryOption(string line)
|
478 | 556 | {
|
479 |
| - return s_sensitivePattern.IsMatch(line) |
480 |
| - ? AddToHistoryOption.MemoryOnly |
481 |
| - : AddToHistoryOption.MemoryAndFile; |
| 557 | + if (string.IsNullOrEmpty(line)) |
| 558 | + { |
| 559 | + return AddToHistoryOption.SkipAdding; |
| 560 | + } |
| 561 | + |
| 562 | + Match match = s_sensitivePattern.Match(line); |
| 563 | + if (ReferenceEquals(match, Match.Empty)) |
| 564 | + { |
| 565 | + return AddToHistoryOption.MemoryAndFile; |
| 566 | + } |
| 567 | + |
| 568 | + bool isSensitive = false; |
| 569 | + Ast ast = string.Equals(_singleton._ast?.Extent.Text, line) |
| 570 | + ? _singleton._ast |
| 571 | + : Parser.ParseInput(line, out _, out _); |
| 572 | + |
| 573 | + do |
| 574 | + { |
| 575 | + int start = match.Index; |
| 576 | + int end = start + match.Length; |
| 577 | + |
| 578 | + IEnumerable<Ast> asts = ast.FindAll( |
| 579 | + ast => ast.Extent.StartOffset <= start && ast.Extent.EndOffset >= end, |
| 580 | + searchNestedScriptBlocks: true); |
| 581 | + |
| 582 | + Ast innerAst = asts.Last(); |
| 583 | + switch (innerAst) |
| 584 | + { |
| 585 | + case VariableExpressionAst: |
| 586 | + // It's a variable with sensitive name. Using the variable is fine, but assigning to |
| 587 | + // the variable could potentially expose sensitive content. |
| 588 | + // If it appears on the left-hand-side of an assignment, and the right-hand-side is |
| 589 | + // not a command invocation, we consider it sensitive. |
| 590 | + // e.g. `$token = Get-Secret` vs. `$token = 'token-text'` or `$token, $url = ...` |
| 591 | + isSensitive = IsOnLeftSideOfAnAssignment(innerAst, out Ast rhs) |
| 592 | + && rhs is not PipelineAst; |
| 593 | + |
| 594 | + if (!isSensitive) |
| 595 | + { |
| 596 | + match = match.NextMatch(); |
| 597 | + } |
| 598 | + break; |
| 599 | + |
| 600 | + case StringConstantExpressionAst strConst: |
| 601 | + // If it's not a command name, or it's not one of the secret management commands that |
| 602 | + // we can ignore, we consider it sensitive. |
| 603 | + isSensitive = !IsSecretMgmtCommand(strConst, out CommandAst command); |
| 604 | + |
| 605 | + if (!isSensitive) |
| 606 | + { |
| 607 | + // We can safely skip the whole command text. |
| 608 | + match = s_sensitivePattern.Match(line, command.Extent.EndOffset); |
| 609 | + } |
| 610 | + break; |
| 611 | + |
| 612 | + case CommandParameterAst param: |
| 613 | + // Special-case the '-AsPlainText' parameter. |
| 614 | + if (string.Equals(param.ParameterName, "AsPlainText")) |
| 615 | + { |
| 616 | + isSensitive = true; |
| 617 | + break; |
| 618 | + } |
| 619 | + |
| 620 | + ExpressionAst arg = GetArgumentForParameter(param); |
| 621 | + if (arg is null) |
| 622 | + { |
| 623 | + // If no argument is found following the parameter, then it could be a switching parameter |
| 624 | + // such as '-UseDefaultPassword' or '-SaveToken', which we assume will not expose sensitive information. |
| 625 | + match = match.NextMatch(); |
| 626 | + } |
| 627 | + else if (arg is VariableExpressionAst) |
| 628 | + { |
| 629 | + // Argument is a variable. It's fine to use a variable for a senstive parameter. |
| 630 | + // e.g. `Invoke-WebRequest -Token $token` |
| 631 | + match = s_sensitivePattern.Match(line, arg.Extent.EndOffset); |
| 632 | + } |
| 633 | + else if (arg is ParenExpressionAst paren |
| 634 | + && paren.Pipeline is PipelineAst pipeline |
| 635 | + && pipeline.PipelineElements[0] is not CommandExpressionAst) |
| 636 | + { |
| 637 | + // Argument is a command invocation, such as `Invoke-WebRequest -Token (Get-Secret)`. |
| 638 | + match = match.NextMatch(); |
| 639 | + } |
| 640 | + else |
| 641 | + { |
| 642 | + // We consider all other arguments sensitive. |
| 643 | + isSensitive = true; |
| 644 | + } |
| 645 | + break; |
| 646 | + |
| 647 | + default: |
| 648 | + isSensitive = true; |
| 649 | + break; |
| 650 | + } |
| 651 | + } |
| 652 | + while (!isSensitive && !ReferenceEquals(match, Match.Empty)); |
| 653 | + |
| 654 | + return isSensitive ? AddToHistoryOption.MemoryOnly : AddToHistoryOption.MemoryAndFile; |
482 | 655 | }
|
483 | 656 |
|
484 | 657 | /// <summary>
|
|
0 commit comments