Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Also it searches for hooks in configured shared hook repositories.
- [Shared Repository Namespace](#shared-repository-namespace)
- [Ignoring Hooks and Files](#ignoring-hooks-and-files)
- [Trusting Hooks](#trusting-hooks)
- [Trusted Remotes](#trusted-remotes)
- [Disabling Githooks](#disabling-githooks)
- [Environment Variables](#environment-variables)
- [Arguments to Shared Hooks](#arguments-to-shared-hooks)
Expand Down Expand Up @@ -644,6 +645,41 @@ for more information.
You can also trust individual hooks by using
[`git hooks trust hooks --help`](docs/cli/git_hooks_trust_hooks.md).

### Trusted Remotes

If you trust all hooks coming from certain remotes, e.g. all repositories of
your own organization, you can add glob patterns which are matched against the
url of the remote `origin` of a repository:

```shell
# Trust all repositories of an organization (for all repositories):
$ git hooks config trusted-remotes --global --add \
'https://github.com/my-org/**' 'git@github.com:my-org/**'
# Show the patterns and if the current repository matches any of them:
$ git hooks config trusted-remotes --print
# Remove all patterns again:
$ git hooks config trusted-remotes --global --reset
```

Every repository whose remote url matches any of these patterns is a trusted
repository, meaning **no trust prompt is shown** and the trust marker file
`<repoPath>/.githooks/trust-all` is not needed. Consult
[`git hooks config trusted-remotes --help`](docs/cli/git_hooks_config_trusted-remotes.md)
for more information. Note the following:

- The url is matched as configured in `remote.origin.url`, meaning
`https://github.com/my-org/repo.git` and `git@github.com:my-org/repo.git` are
different urls which need separate patterns. A repository without a remote
`origin` is never trusted.
- The separator is always `/`, therefore `*` does not match over `/` but `**`
does.
- A repository whose trust setting was explicitly set by the user (see
[`git hooks config trust-all`](docs/cli/git_hooks_config_trust-all.md)) is not
affected by these patterns, meaning a denied repository stays untrusted.
- Since all current **and future** hooks of matching repositories run without
any confirmation, only add remotes you fully trust: anybody who can push hooks
to such a repository can execute code on your machine.

## Disabling Githooks

To disable running any Githooks locally or globally, use the following:
Expand Down
2 changes: 2 additions & 0 deletions docs/cli/git_hooks_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ git hooks config
Enable/disable skipping active, untrusted hooks.
- [git hooks config trust-all](git_hooks_config_trust-all.md) - Change trust
settings in the current repository.
- [git hooks config trusted-remotes](git_hooks_config_trusted-remotes.md) -
Updates the list of trusted remotes.
- [git hooks config update-check](git_hooks_config_update-check.md) - Change
Githooks update-check settings.
- [git hooks config update-time](git_hooks_config_update-time.md) - Changes the
Expand Down
47 changes: 47 additions & 0 deletions docs/cli/git_hooks_config_trusted-remotes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
## git hooks config trusted-remotes

Updates the list of trusted remotes.

### Synopsis

Updates the list of glob patterns which are matched against the url of the
remote `origin` of a repository.

Every repository whose remote url matches any of these patterns trusts all its
current and future hooks automatically, meaning no trust prompt is shown and the
trust marker file `<repoPath>/.githooks/trust-all` is not needed.

The url is matched as configured in `remote.origin.url`, meaning
`https://github.com/org/repo.git` and 'git@github.com:org/repo.git' are
different urls which may need separate patterns. The separator is always `/`,
therefore '\*' does not match over `/` but '\*\*' does.

A repository whose trust setting was explicitly set by the user (see
`git hooks config trust-all`) is not affected by these patterns.

Only add remotes whose current and future hooks you fully trust, since Githooks
will run them without any confirmation.

The `--add` option accepts multiple `<pattern>` arguments.

```
git hooks config trusted-remotes [flags] [<pattern>...]
```

### Options

```
--local Use the local Git configuration.
--global Use the global Git configuration (default).
--print Print the setting.
--add Adds given trusted remote patterns `<pattern>`s.
--reset Reset the setting.
-h, --help help for trusted-remotes
```

### SEE ALSO

- [git hooks config](git_hooks_config.md) - Manages various Githooks
configuration.

###### Auto generated by spf13/cobra
115 changes: 115 additions & 0 deletions githooks/cmd/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,67 @@ func runSharedRepos(ctx *ccm.CmdContext, opts *SetOptions, gitOpts *GitOptions)
}
}

func runTrustedRemotes(ctx *ccm.CmdContext, opts *SetOptions, gitOpts *GitOptions) {
opt := hooks.GitCKTrustedRemotes

localOrGlobal := "local"
if gitOpts.Global {
localOrGlobal = "global"
}

switch {
case opts.Set:
scope := wrapToGitScope(ctx.Log, gitOpts)
for i := range opts.Values {
err := ctx.GitX.AddConfig(opt, opts.Values[i], scope)
ctx.Log.AssertNoErrorPanicF(err, "Could not add %s trusted remote.", localOrGlobal)
}
ctx.Log.InfoF("Added '%v' %s trusted remotes.", len(opts.Values), localOrGlobal)

case opts.Reset:
scope := wrapToGitScope(ctx.Log, gitOpts)
err := ctx.GitX.UnsetConfig(opt, scope)
ctx.Log.AssertNoErrorPanicF(err, "Could not unset %s trusted remotes.", localOrGlobal)
ctx.Log.InfoF("Removed all %s trusted remotes.", localOrGlobal)

case opts.Print:
list := func(p []string) string {
if len(p) == 0 {
return "[0]: none"
}

return strs.Fmt("[%v]:\n%s", len(p),
strings.Join(strs.Map(p,
func(s string) string { return strs.Fmt("%s '%s'", cm.ListItemLiteral, s) }),
"\n"))
}

if gitOpts.Local {
ctx.Log.InfoF("Local trusted remotes %s",
list(ctx.GitX.GetConfigAll(opt, git.LocalScope)))
}

if gitOpts.Global {
ctx.Log.InfoF("Global trusted remotes %s",
list(ctx.GitX.GetConfigAll(opt, git.GlobalScope)))
}

// Report the effect on the current repository, if we are inside one.
if _, _, _, err := ctx.GitX.GetRepoRoot(); err == nil {
if isTrusted, pattern := hooks.IsRemoteTrusted(ctx.GitX); isTrusted {
ctx.Log.InfoF(
"The current repository is trusted by pattern '%s'.", pattern)
} else {
ctx.Log.Info(
"The current repository is not trusted by any trusted remote.")
}
}

default:
cm.Panic("Wrong arguments.")
}
}

func runCloneURL(ctx *ccm.CmdContext, opts *SetOptions) {
switch {
case opts.Set:
Expand Down Expand Up @@ -933,6 +994,58 @@ each containing a clone URL of a shared hook repository which gets added.`,
configCmd.AddCommand(ccm.SetCommandDefaults(ctx.Log, sharedCmd))
}

func configTrustedRemotesCmd(
ctx *ccm.CmdContext,
configCmd *cobra.Command,
setOpts *SetOptions,
gitOpts *GitOptions,
) {
trustedRemotesCmd := &cobra.Command{
Use: "trusted-remotes [flags] [<pattern>...]",
Short: "Updates the list of trusted remotes.",
Long: `Updates the list of glob patterns which are matched against
the url of the remote '` + hooks.TrustedRemoteName + `' of a repository.

Every repository whose remote url matches any of these patterns trusts all
its current and future hooks automatically, meaning no trust prompt is shown
and the trust marker file '<repoPath>/` + hooks.HooksDirName + `/trust-all' is not needed.

The url is matched as configured in 'remote.` + hooks.TrustedRemoteName + `.url',
meaning 'https://github.com/org/repo.git' and 'git@github.com:org/repo.git' are
different urls which may need separate patterns. The separator is always '/',
therefore '*' does not match over '/' but '**' does.

A repository whose trust setting was explicitly set by the user
(see 'git hooks config trust-all') is not affected by these patterns.

Only add remotes whose current and future hooks you fully trust, since
Githooks will run them without any confirmation.

The '--add' option accepts multiple '<pattern>' arguments.`,
Run: func(cmd *cobra.Command, args []string) {
if !gitOpts.Local && !gitOpts.Global {
_, _, _, err := ctx.GitX.GetRepoRoot()
gitOpts.Global = true
gitOpts.Local = setOpts.Print && err == nil
} else if gitOpts.Local {
ccm.AssertRepoRoot(ctx)
}

runTrustedRemotes(ctx, setOpts, gitOpts)
}}

optsPSR := createOptionMap(true, false, true)
optsPSR.Set = "add"
optsPSR.SetDesc = "Adds given trusted remote patterns '<pattern>'s."
trustedRemotesCmd.Flags().
BoolVar(&gitOpts.Local, "local", false, "Use the local Git configuration.")
trustedRemotesCmd.Flags().
BoolVar(&gitOpts.Global, "global", false, "Use the global Git configuration (default).")

configSetOptions(trustedRemotesCmd, setOpts, &optsPSR, ctx.Log, 1, -1)
configCmd.AddCommand(ccm.SetCommandDefaults(ctx.Log, trustedRemotesCmd))
}

func configSkipNonExistingSharedHooks(
ctx *ccm.CmdContext,
configCmd *cobra.Command,
Expand Down Expand Up @@ -1142,6 +1255,8 @@ func NewCmd(ctx *ccm.CmdContext) *cobra.Command {
configSharedCmd(ctx, configCmd, &setOpts, &gitOpts)
configDisableSharedHooksUpdate(ctx, configCmd, &setOpts, &gitOpts)

configTrustedRemotesCmd(ctx, configCmd, &setOpts, &gitOpts)

configSkipNonExistingSharedHooks(ctx, configCmd, &setOpts, &gitOpts)
configFailUntrustedHooks(ctx, configCmd, &setOpts, &gitOpts)

Expand Down
8 changes: 8 additions & 0 deletions githooks/common/glob.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ func GlobMatch(pattern string, path string) (bool, error) {
return glob.Match(pattern, path)
}

// GlobMatchSlashes matches a pattern against a string which is always
// separated by forward slashes `/`, such as an url.
// In contrast to `GlobMatch` the result does not depend on the platforms
// path separator, meaning `*` never matches over `/` and `**` does.
func GlobMatchSlashes(pattern string, s string) (bool, error) {
return glob.Match(pattern, s)
}

// Globs represents one filepath glob, with its elements joined by "**".
type globs []string

Expand Down
6 changes: 6 additions & 0 deletions githooks/hooks/gitconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ const (
GitCKSkipNonExistingSharedHooks = "githooks.skipNonExistingSharedHooks"
GitCKSkipUntrustedHooks = "githooks.skipUntrustedHooks"

GitCKTrustedRemotes = "githooks.trustedRemotes"

GitCKRunnerIsNonInteractive = "githooks.runnerIsNonInteractive"

GitCKContainerizedHooksEnabled = "githooks.containerizedHooksEnabled"
Expand Down Expand Up @@ -96,6 +98,8 @@ func GetGlobalGitConfigKeys() []string {
GitCKSkipNonExistingSharedHooks,
GitCKSkipUntrustedHooks,

GitCKTrustedRemotes,

GitCKRunnerIsNonInteractive,

GitCKContainerManager,
Expand All @@ -120,6 +124,8 @@ func GetLocalGitConfigKeys() []string {
GitCKSkipNonExistingSharedHooks,
GitCKSkipUntrustedHooks,

GitCKTrustedRemotes,

GitCKRunnerIsNonInteractive,

GitCKContainerManager,
Expand Down
74 changes: 72 additions & 2 deletions githooks/hooks/trusted.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,73 @@ func SetTrustAllSetting(gitx *git.Context, enable bool, reset bool) error {
}
}

// TrustedRemoteName is the name of the remote whose url is matched
// against the trusted remote patterns (see `IsRemoteTrusted`).
const TrustedRemoteName = "origin"

// GetTrustedRemotes gets the trusted remote url patterns in `scope`.
func GetTrustedRemotes(gitx *git.Context, scope git.ConfigScope) []string {
return gitx.GetConfigAll(GitCKTrustedRemotes, scope)
}

// matchesTrustedRemote reports if `url` matches any glob pattern in
// `patterns` together with the first pattern which matched.
// An empty `url` never matches, such that a repository without a
// remote is never trusted by a pattern like `*`.
func matchesTrustedRemote(patterns []string, url string) (isTrusted bool, pattern string) {
if strs.IsEmpty(url) {
return
}

for _, p := range patterns {
if strs.IsEmpty(p) {
continue
}

// Urls are always separated by `/`, therefore match
// platform independent of the path separator.
matched, err := cm.GlobMatchSlashes(p, url)
cm.DebugAssertNoErrorF(err, "Malformed trusted remote pattern '%s'.", p)

if err != nil {
continue
}

if matched {
return true, p
}
}

return
}

// IsRemoteTrusted tells if the url of the remote `TrustedRemoteName` of the
// current repository matches any pattern in the trusted remotes
// configuration `GitCKTrustedRemotes` together with the pattern which matched.
// The url is matched as configured, meaning e.g. `https://` and `ssh://` urls
// of the same repository need separate patterns.
func IsRemoteTrusted(gitx *git.Context) (isTrusted bool, pattern string) {
patterns := GetTrustedRemotes(gitx, git.Traverse)
if len(patterns) == 0 {
return
}

return matchesTrustedRemote(
patterns,
gitx.GetConfig("remote."+TrustedRemoteName+".url", git.LocalScope))
}

// IsRepoTrusted tells if the repository `repoPath` is trusted.
// It is only trusted if the trust marker is present and
// the `trustAll` settings is set to `trusted`.
// It is trusted if either
// - the trust marker is present and the `trustAll` setting is set to
// `trusted`, or
// - the url of the remote `TrustedRemoteName` matches any trusted remote
// pattern (see `IsRemoteTrusted`), which needs neither the trust marker
// nor any user interaction.
//
// An explicit `trustAll` setting in the repository always takes precedence
// over the trusted remotes configuration, meaning a repository whose trust
// was denied by the user stays untrusted.
// On any error `false` is reported together with the error.
func IsRepoTrusted(
gitx *git.Context,
Expand All @@ -49,6 +113,12 @@ func IsRepoTrusted(
isTrusted, trustAllSet = GetTrustAllSetting(gitx)
}

if isTrusted || trustAllSet {
return
}

isTrusted, _ = IsRemoteTrusted(gitx)

return
}

Expand Down
Loading