PowerShell / PowerShell

PowerShell for every system!
https://microsoft.com/PowerShell
MIT License
44.66k stars 7.23k forks source link

PowerShell should eventfully expose command creation #20477

Open StartAutomating opened 11 months ago

StartAutomating commented 11 months ago

Summary of the new feature / enhancement

As a developer, I want to be able to know when a new PowerShell command is created.

There are a variety of reasons where this would prove useful:

  1. Registration of functions (for extensibility purpose)
  2. Prevalidation of functions (for more immediate feedback, since functions will not error until they execute)
  3. Logging

Proposed technical implementation details (optional)

Ideally, the PowerShell engine should expose engine events containing this information.

If the team does not wish to generate engine events without being sure a listener will care, events could be surfaced from a number of classes.

SeeminglyScience commented 11 months ago

Possible duplicate of #13493. What do you think @StartAutomating?

StartAutomating commented 11 months ago

@SeeminglyScience they would both be good.

Module load being an event would be valuable for module-to-module integrations (aka, your module plugs into my module, and being notified that a module loads lets me know I should attempt to import extensions from the new module)

Command being loaded is more granular and flexible.

For one, not all commands will be in a module (this should notify even if a function is created on the cli / within another function)

For another, providing a "module" event would require me to loop thru each command in the module to check for handshakes, rather than allowing the command itself to be the handshake.

That stated, I think the code required to support both scenarios would be similar, and, if I construct a fix, I'll try to do both.

StartAutomating commented 11 months ago

@SeeminglyScience a related curiosity is this:

Can any of the PowerShell engine auditing stuff be efficiently repurposed towards these two points?

IISResetMe commented 8 months ago

Hey @StartAutomating

We discussed your proposal in the Engine Working Group earlier this week, but we struggled a bit with how to interpret the expected outcome here. Based on your input we assumed it unlikely that you'd be interested in tracing output - you want to be able to react to (and possibly intercept) function registration at runtime from within the shell - is this correctly understood?

In that case you'd need a facility for hooking an event handler. Let's say, hypothetically, we decide to implement Register-ScopedEngineEvent for this purpose, and provide a cognate runtime API to handle the event registrations.

Now, the primary source of complexity for this ask is that function registrations are scoped in PowerShell. Consider the following example:

& {
  Register-ScopedEngineEvent -SourceIdentifier FunctionRegistration -Action {
    param($sender,$scopedEngineEventArgs)
    Write-Host "Function $($scopedEngineEventArgs.Name) is about to be defined in local scope of scriptblock $(scopedEngineEventArgs.ScriptBlockId)"
  }

  function foo { } # I assume you want evaluation of this statement raise the event
  & {
    function bar {}  # but what about this one?
  }

  function fooDeep {
    function fooDeepHelper {} # what about this one?

    fooDeepHelper
  }

  fooDeep
  fooDeep # now `function fooDeepHelper {}` has been registered twice in different local scopes 
          # do you want to know every time?
}

In other words: what behavior would you expect from such a facility :)

StartAutomating commented 8 months ago

@IISResetMe my apologies, I had meant to get around to a brief PR on this one. I'll see what I can do.

I wasn't expecting a new cmdlet, just a new engine event (like "PowerShell.Exiting" )

I believe the engine event would need to have event arguments or message data comprising of:

Does this make the desired implementation clear?

I'm happy to discuss directly with the working group if you would like and will see if I can craft a proof-of-concept.

Hope this Helps!

IISResetMe commented 8 months ago

I'm still gonna need some clarification regarding scope propagation - unless the idea is that any caller from any call stack can subscribe to these events and get them from ... any function registration anywhere within the attached runspace?

StartAutomating commented 8 months ago

@IISResetMe I think the simplest answer would be to allow it from anywhere within the attached runspace.

I think in your example, yes, you would want to know "every time" fooDeepHelper has been registered. I'd also want to know "every time" an alias is created at any of those scopes, too.

If we're thinking about this from the security/logging perspective only, getting selective about scope is setting up places where commands can hide. And, surely, we do not want that.

Make sense?

LMK if you need more info or clarification.

IISResetMe commented 6 months ago

I had a look at the relevant code for evaluation of function/filter statements in the compiler, and we can trivially implement an engine event that emits post-eval, for the hosting runspace, allowing you to do something like:

Register-EngineEvent -SourceIdentifier PowerShell.SetFunction -Action {
  Write-Host "Registered function with name: $($EventArgs.Name)"        # [string]
  Write-Host "Function body: $($EventArgs.ScriptBlock)"                 # [scriptblock]
  Write-Host "Resulting CommandInfo object: $($EventArgs.CommandInfo)"  # [FunctionInfo]
  Write-Host "Registered aliases: $($EventArgs.Aliases)"                # [AliasInfo[]]
  Write-Host "Function already existed? $($EventArgs.Updated)"          # [bool]
}

It would then be raised every time a statement using the function or filter keywords would be evaluated in the current runspace:

function f { } # 1 event is emitted

1..10 |ForEach-Object {
  function f { }    # 10 events eventually emitted
  f
} 

Would this be sufficient for the use cases you have in mind @StartAutomating ? My only concern with this approach is that we might want to provide a way to filter for new vs updated functions, to reduce noise when you're only interested in initial registration.

(Note: direct modification of items or content in the function: provider drive would bypass the emission of the event)

StartAutomating commented 6 months ago

@IISResetMe this seems like it would handle about 90%+ of the scenarios.

While I do use the function: provider regularly enough, I also can see some benefit to having an "exception to the rule" (except for the potential security downside of not knowing about that new function).

I also see the benefit of providing two events (new vs updated).

So, in short, sounds pretty good.

My suggested event IDs are:

Though I see little harm in being a little "alias happy" here.

StartAutomating commented 2 weeks ago

@IISResetMe did you ever get the chance to make a PR for the engine event?