mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-14 09:58:09 -05:00
587 lines
18 KiB
Go
587 lines
18 KiB
Go
// MIT License
|
|
|
|
// Copyright (c) 2019 Zachary Rice
|
|
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
|
|
// The above copyright notice and this permission notice shall be included in all
|
|
// copies or substantial portions of the Software.
|
|
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
_ "embed"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Infisical/infisical-merge/config"
|
|
"github.com/Infisical/infisical-merge/detect"
|
|
"github.com/Infisical/infisical-merge/packages/util"
|
|
"github.com/Infisical/infisical-merge/report"
|
|
"github.com/manifoldco/promptui"
|
|
"github.com/posthog/posthog-go"
|
|
"github.com/rs/zerolog/log"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
const configDescription = `config file path
|
|
order of precedence:
|
|
1. --config flag
|
|
2. env var INFISICAL_SCAN_CONFIG
|
|
3. (--source/-s)/.infisical-scan.toml
|
|
If none of the three options are used, then Infisical will use the default scan config`
|
|
|
|
//go:embed pre-commit-script/pre-commit.sh
|
|
var preCommitTemplate []byte
|
|
|
|
//go:embed pre-commit-script/pre-commit-without-bang.sh
|
|
var preCommitTemplateAppend []byte
|
|
|
|
const (
|
|
defaultHooksPath = ".git/hooks/"
|
|
preCommitFile = "pre-commit"
|
|
)
|
|
|
|
func init() {
|
|
// scan flag for only scan command
|
|
scanCmd.Flags().String("log-opts", "", "git log options")
|
|
scanCmd.Flags().Bool("no-git", false, "treat git repo as a regular directory and scan those files, --log-opts has no effect on the scan when --no-git is set")
|
|
scanCmd.Flags().Bool("pipe", false, "scan input from stdin, ex: `cat some_file | infisical scan --pipe`")
|
|
scanCmd.Flags().Bool("follow-symlinks", false, "scan files that are symlinks to other files")
|
|
|
|
// global scan flags
|
|
scanCmd.PersistentFlags().StringP("config", "c", "", configDescription)
|
|
scanCmd.PersistentFlags().Int("exit-code", 1, "exit code when leaks have been encountered")
|
|
scanCmd.PersistentFlags().StringP("source", "s", ".", "path to source")
|
|
scanCmd.PersistentFlags().StringP("report-path", "r", "", "report file")
|
|
scanCmd.PersistentFlags().StringP("report-format", "f", "json", "output format (json, csv, sarif)")
|
|
scanCmd.PersistentFlags().StringP("baseline-path", "b", "", "path to baseline with issues that can be ignored")
|
|
scanCmd.PersistentFlags().BoolP("verbose", "v", false, "show verbose output from scan (which file, where in the file, what secret)")
|
|
scanCmd.PersistentFlags().BoolP("no-color", "", false, "turn off color for verbose output")
|
|
scanCmd.PersistentFlags().Int("max-target-megabytes", 0, "files larger than this will be skipped")
|
|
scanCmd.PersistentFlags().Bool("redact", false, "redact secrets from logs and stdout")
|
|
|
|
// scan git changes command flags
|
|
scanGitChangesCmd.Flags().Bool("staged", false, "detect secrets in a --staged state")
|
|
scanGitChangesCmd.Flags().String("log-opts", "", "git log options")
|
|
|
|
// find config source
|
|
err := viper.BindPFlag("config", scanCmd.PersistentFlags().Lookup("config"))
|
|
if err != nil {
|
|
log.Fatal().Msgf("err binding config %s", err.Error())
|
|
}
|
|
|
|
// add flags to main
|
|
scanCmd.AddCommand(scanGitChangesCmd)
|
|
rootCmd.AddCommand(scanCmd)
|
|
|
|
installCmd.Flags().Bool("pre-commit-hook", false, "installs pre commit hook for Git repository")
|
|
scanCmd.AddCommand(installCmd)
|
|
}
|
|
|
|
func initScanConfig(cmd *cobra.Command) {
|
|
cfgPath, err := cmd.Flags().GetString("config")
|
|
if err != nil {
|
|
log.Fatal().Msg(err.Error())
|
|
}
|
|
|
|
if cfgPath != "" {
|
|
viper.SetConfigFile(cfgPath)
|
|
log.Debug().Msgf("using scan config %s from `--config`", cfgPath)
|
|
} else if os.Getenv(config.DefaultScanConfigEnvName) != "" {
|
|
envPath := os.Getenv(config.DefaultScanConfigEnvName)
|
|
viper.SetConfigFile(envPath)
|
|
log.Debug().Msgf("using scan config from %s env var: %s", config.DefaultScanConfigEnvName, envPath)
|
|
} else {
|
|
source, err := cmd.Flags().GetString("source")
|
|
if err != nil {
|
|
log.Fatal().Msg(err.Error())
|
|
}
|
|
fileInfo, err := os.Stat(source)
|
|
if err != nil {
|
|
log.Fatal().Msg(err.Error())
|
|
}
|
|
|
|
if !fileInfo.IsDir() {
|
|
log.Debug().Msgf("unable to load scan config from %s since --source=%s is a file, using default config",
|
|
filepath.Join(source, config.DefaultScanConfigFileName), source)
|
|
viper.SetConfigType("toml")
|
|
if err = viper.ReadConfig(strings.NewReader(config.DefaultConfig)); err != nil {
|
|
log.Fatal().Msgf("err reading toml %s", err.Error())
|
|
}
|
|
return
|
|
}
|
|
|
|
if _, err := os.Stat(filepath.Join(source, config.DefaultScanConfigFileName)); os.IsNotExist(err) {
|
|
log.Debug().Msgf("no scan config found in path %s, using default scan config", filepath.Join(source, config.DefaultScanConfigFileName))
|
|
viper.SetConfigType("toml")
|
|
if err = viper.ReadConfig(strings.NewReader(config.DefaultConfig)); err != nil {
|
|
log.Fatal().Msgf("err reading default scan config toml %s", err.Error())
|
|
}
|
|
return
|
|
} else {
|
|
log.Debug().Msgf("using existing scan config %s from `(--source)/%s`", filepath.Join(source, config.DefaultScanConfigFileName), config.DefaultScanConfigFileName)
|
|
}
|
|
|
|
viper.AddConfigPath(source)
|
|
viper.SetConfigName(config.DefaultScanConfigFileName)
|
|
viper.SetConfigType("toml")
|
|
}
|
|
if err := viper.ReadInConfig(); err != nil {
|
|
log.Fatal().Msgf("unable to load scan config, err: %s", err)
|
|
}
|
|
}
|
|
|
|
var installCmd = &cobra.Command{
|
|
Use: "install",
|
|
Short: "Install scanning scripts and tools. Use --help flag to see all options",
|
|
Args: cobra.ExactArgs(0),
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
installPrecommit := cmd.Flags().Changed("pre-commit-hook")
|
|
if installPrecommit {
|
|
hooksPath, err := getHooksPath()
|
|
if err != nil {
|
|
fmt.Printf("Error: %s\n", err)
|
|
return
|
|
}
|
|
|
|
if hooksPath != ".git/hooks" {
|
|
defaultHookOverride, err := overrideDefaultHooksPath(hooksPath)
|
|
if err != nil {
|
|
fmt.Printf("Error: %s\n", err)
|
|
}
|
|
|
|
if defaultHookOverride {
|
|
ConfigureGitHooksPath()
|
|
|
|
log.Info().Msgf("To switch back previous githooks manager run: git config core.hooksPath %s\n", hooksPath)
|
|
return
|
|
} else {
|
|
log.Warn().Msgf("To automatically configure this hook, you need to switch the path of the Hooks. Alternatively, you can manually configure this hook by setting your pre-commit script to run command [infisical scan git-changes -v --staged].\n")
|
|
return
|
|
}
|
|
}
|
|
|
|
err = createOrUpdatePreCommitFile(hooksPath)
|
|
if err != nil {
|
|
fmt.Printf("Error: %s\n", err)
|
|
return
|
|
}
|
|
|
|
log.Info().Msgf("Pre-commit hook successfully added. Infisical scan should now run on each commit you make\n")
|
|
|
|
Telemetry.CaptureEvent("cli-command:install --pre-commit-hook", posthog.NewProperties().Set("version", util.CLI_VERSION))
|
|
|
|
return
|
|
}
|
|
}}
|
|
|
|
var scanCmd = &cobra.Command{
|
|
Use: "scan",
|
|
Short: "Scan for leaked secrets in git history, directories, and files",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
initScanConfig(cmd)
|
|
|
|
var (
|
|
vc config.ViperConfig
|
|
findings []report.Finding
|
|
err error
|
|
)
|
|
|
|
// Load config
|
|
if err = viper.Unmarshal(&vc); err != nil {
|
|
log.Fatal().Err(err).Msg("Failed to load config")
|
|
}
|
|
cfg, err := vc.Translate()
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("Failed to load config")
|
|
}
|
|
cfg.Path, _ = cmd.Flags().GetString("config")
|
|
|
|
// start timer
|
|
start := time.Now()
|
|
|
|
// Setup detector
|
|
detector := detect.NewDetector(cfg)
|
|
detector.Config.Path, err = cmd.Flags().GetString("config")
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("")
|
|
}
|
|
source, err := cmd.Flags().GetString("source")
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("")
|
|
}
|
|
// if config path is not set, then use the {source}/.infisical-scan.toml path.
|
|
// note that there may not be a `{source}/.infisical-scan.toml` file, this is ok.
|
|
if detector.Config.Path == "" {
|
|
detector.Config.Path = filepath.Join(source, config.DefaultScanConfigFileName)
|
|
}
|
|
// set verbose flag
|
|
if detector.Verbose, err = cmd.Flags().GetBool("verbose"); err != nil {
|
|
log.Fatal().Err(err).Msg("")
|
|
}
|
|
// set redact flag
|
|
if detector.Redact, err = cmd.Flags().GetBool("redact"); err != nil {
|
|
log.Fatal().Err(err).Msg("")
|
|
}
|
|
if detector.MaxTargetMegaBytes, err = cmd.Flags().GetInt("max-target-megabytes"); err != nil {
|
|
log.Fatal().Err(err).Msg("")
|
|
}
|
|
// set color flag
|
|
if detector.NoColor, err = cmd.Flags().GetBool("no-color"); err != nil {
|
|
log.Fatal().Err(err).Msg("")
|
|
}
|
|
|
|
if fileExists(filepath.Join(source, config.DefaultInfisicalIgnoreFineName)) {
|
|
if err = detector.AddGitleaksIgnore(filepath.Join(source, config.DefaultInfisicalIgnoreFineName)); err != nil {
|
|
log.Fatal().Err(err).Msg("could not call AddInfisicalIgnore")
|
|
}
|
|
}
|
|
|
|
// ignore findings from the baseline (an existing report in json format generated earlier)
|
|
baselinePath, _ := cmd.Flags().GetString("baseline-path")
|
|
if baselinePath != "" {
|
|
err = detector.AddBaseline(baselinePath, source)
|
|
if err != nil {
|
|
log.Error().Msgf("Could not load baseline. The path must point to report generated by `infisical scan` using the default format: %s", err)
|
|
}
|
|
}
|
|
|
|
// set follow symlinks flag
|
|
if detector.FollowSymlinks, err = cmd.Flags().GetBool("follow-symlinks"); err != nil {
|
|
log.Fatal().Err(err).Msg("")
|
|
}
|
|
|
|
// set exit code
|
|
exitCode, err := cmd.Flags().GetInt("exit-code")
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("could not get exit code")
|
|
}
|
|
|
|
// determine what type of scan:
|
|
// - git: scan the history of the repo
|
|
// - no-git: scan files by treating the repo as a plain directory
|
|
noGit, err := cmd.Flags().GetBool("no-git")
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("could not call GetBool() for no-git")
|
|
}
|
|
fromPipe, err := cmd.Flags().GetBool("pipe")
|
|
if err != nil {
|
|
log.Fatal().Err(err)
|
|
}
|
|
|
|
log.Info().Msgf("scanning for exposed secrets...")
|
|
|
|
// start the detector scan
|
|
if noGit {
|
|
findings, err = detector.DetectFiles(source)
|
|
if err != nil {
|
|
// don't exit on error, just log it
|
|
log.Error().Err(err).Msg("")
|
|
}
|
|
} else if fromPipe {
|
|
findings, err = detector.DetectReader(os.Stdin, 10)
|
|
if err != nil {
|
|
// log fatal to exit, no need to continue since a report
|
|
// will not be generated when scanning from a pipe...for now
|
|
log.Fatal().Err(err).Msg("")
|
|
}
|
|
} else {
|
|
var logOpts string
|
|
logOpts, err = cmd.Flags().GetString("log-opts")
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("")
|
|
}
|
|
findings, err = detector.DetectGit(source, logOpts, detect.DetectType)
|
|
if err != nil {
|
|
// don't exit on error, just log it
|
|
log.Error().Err(err).Msg("")
|
|
}
|
|
}
|
|
|
|
// log info about the scan
|
|
if err == nil {
|
|
log.Info().Msgf("scan completed in %s", FormatDuration(time.Since(start)))
|
|
if len(findings) != 0 {
|
|
log.Warn().Msgf("leaks found: %d", len(findings))
|
|
} else {
|
|
log.Info().Msg("no leaks found")
|
|
}
|
|
} else {
|
|
log.Warn().Msgf("partial scan completed in %s", FormatDuration(time.Since(start)))
|
|
if len(findings) != 0 {
|
|
log.Warn().Msgf("%d leaks found in partial scan", len(findings))
|
|
} else {
|
|
log.Warn().Msg("no leaks found in partial scan")
|
|
}
|
|
}
|
|
|
|
Telemetry.CaptureEvent("cli-command:scan", posthog.NewProperties().Set("risks", len(findings)).Set("version", util.CLI_VERSION))
|
|
|
|
// write report if desired
|
|
reportPath, _ := cmd.Flags().GetString("report-path")
|
|
ext, _ := cmd.Flags().GetString("report-format")
|
|
if reportPath != "" {
|
|
if err := report.Write(findings, cfg, ext, reportPath); err != nil {
|
|
log.Fatal().Err(err).Msg("could not write")
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
os.Exit(1)
|
|
}
|
|
|
|
if len(findings) != 0 {
|
|
os.Exit(exitCode)
|
|
}
|
|
},
|
|
}
|
|
|
|
var scanGitChangesCmd = &cobra.Command{
|
|
Use: "git-changes",
|
|
Short: "Scan for secrets in uncommitted changes in a git repo",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
initScanConfig(cmd)
|
|
|
|
var vc config.ViperConfig
|
|
|
|
if err := viper.Unmarshal(&vc); err != nil {
|
|
log.Fatal().Err(err).Msg("Failed to load config")
|
|
}
|
|
cfg, err := vc.Translate()
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("Failed to load config")
|
|
}
|
|
|
|
cfg.Path, _ = cmd.Flags().GetString("config")
|
|
exitCode, _ := cmd.Flags().GetInt("exit-code")
|
|
staged, _ := cmd.Flags().GetBool("staged")
|
|
start := time.Now()
|
|
|
|
// Setup detector
|
|
detector := detect.NewDetector(cfg)
|
|
detector.Config.Path, err = cmd.Flags().GetString("config")
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("")
|
|
}
|
|
source, err := cmd.Flags().GetString("source")
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("")
|
|
}
|
|
// if config path is not set, then use the {source}/.infisical-scan.toml path.
|
|
// note that there may not be a `{source}/.infisical-scan.toml` file, this is ok.
|
|
if detector.Config.Path == "" {
|
|
detector.Config.Path = filepath.Join(source, config.DefaultScanConfigFileName)
|
|
}
|
|
// set verbose flag
|
|
if detector.Verbose, err = cmd.Flags().GetBool("verbose"); err != nil {
|
|
log.Fatal().Err(err).Msg("")
|
|
}
|
|
// set redact flag
|
|
if detector.Redact, err = cmd.Flags().GetBool("redact"); err != nil {
|
|
log.Fatal().Err(err).Msg("")
|
|
}
|
|
if detector.MaxTargetMegaBytes, err = cmd.Flags().GetInt("max-target-megabytes"); err != nil {
|
|
log.Fatal().Err(err).Msg("")
|
|
}
|
|
// set color flag
|
|
if detector.NoColor, err = cmd.Flags().GetBool("no-color"); err != nil {
|
|
log.Fatal().Err(err).Msg("")
|
|
}
|
|
|
|
if fileExists(filepath.Join(source, config.DefaultInfisicalIgnoreFineName)) {
|
|
if err = detector.AddGitleaksIgnore(filepath.Join(source, config.DefaultInfisicalIgnoreFineName)); err != nil {
|
|
log.Fatal().Err(err).Msg("could not call AddInfisicalIgnore")
|
|
}
|
|
}
|
|
|
|
// get log options for git scan
|
|
logOpts, err := cmd.Flags().GetString("log-opts")
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("")
|
|
}
|
|
|
|
log.Info().Msgf("scanning for exposed secrets...")
|
|
|
|
// start git scan
|
|
var findings []report.Finding
|
|
if staged {
|
|
findings, err = detector.DetectGit(source, logOpts, detect.ProtectStagedType)
|
|
} else {
|
|
findings, err = detector.DetectGit(source, logOpts, detect.ProtectType)
|
|
}
|
|
if err != nil {
|
|
// don't exit on error, just log it
|
|
log.Error().Err(err).Msg("")
|
|
}
|
|
|
|
// log info about the scan
|
|
log.Info().Msgf("scan completed in %s", FormatDuration(time.Since(start)))
|
|
if len(findings) != 0 {
|
|
log.Warn().Msgf("leaks found: %d", len(findings))
|
|
} else {
|
|
log.Info().Msg("no leaks found")
|
|
}
|
|
|
|
Telemetry.CaptureEvent("cli-command:scan git-changes", posthog.NewProperties().Set("risks", len(findings)).Set("version", util.CLI_VERSION))
|
|
|
|
reportPath, _ := cmd.Flags().GetString("report-path")
|
|
ext, _ := cmd.Flags().GetString("report-format")
|
|
if reportPath != "" {
|
|
if err = report.Write(findings, cfg, ext, reportPath); err != nil {
|
|
log.Fatal().Err(err).Msg("")
|
|
}
|
|
}
|
|
if len(findings) != 0 {
|
|
os.Exit(exitCode)
|
|
}
|
|
},
|
|
}
|
|
|
|
func fileExists(fileName string) bool {
|
|
// check for a .infisicalignore file
|
|
info, err := os.Stat(fileName)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
|
|
if info != nil && err == nil {
|
|
if !info.IsDir() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func FormatDuration(d time.Duration) string {
|
|
scale := 100 * time.Second
|
|
// look for the max scale that is smaller than d
|
|
for scale > d {
|
|
scale = scale / 10
|
|
}
|
|
return d.Round(scale / 100).String()
|
|
}
|
|
|
|
func overrideDefaultHooksPath(managedHook string) (bool, error) {
|
|
YES := "Yes"
|
|
NO := "No"
|
|
|
|
options := []string{YES, NO}
|
|
optionsPrompt := promptui.Select{
|
|
Label: fmt.Sprintf("Your hooks path is set to [%s] but needs to be [.git/hooks] for automatic configuration. Would you like to switch? ", managedHook),
|
|
Items: options,
|
|
Size: 2,
|
|
}
|
|
|
|
_, selectedOption, err := optionsPrompt.Run()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return selectedOption == YES, err
|
|
}
|
|
|
|
func ConfigureGitHooksPath() {
|
|
cmd := exec.Command("git", "config", "core.hooksPath", ".git/hooks")
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
log.Fatal().Msgf("Failed to configure git hooks path: %v", err)
|
|
}
|
|
}
|
|
|
|
// GetGitRoot returns the root directory of the current Git repository.
|
|
func GetGitRoot() (string, error) {
|
|
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
|
|
output, err := cmd.Output()
|
|
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get git root directory: %w", err)
|
|
}
|
|
|
|
gitRoot := strings.TrimSpace(string(output)) // Remove any trailing newline
|
|
return gitRoot, nil
|
|
}
|
|
|
|
func getHooksPath() (string, error) {
|
|
out, err := exec.Command("git", "config", "core.hooksPath").Output()
|
|
if err != nil {
|
|
if len(out) == 0 {
|
|
out = []byte(".git/hooks") // set the default hook
|
|
} else {
|
|
log.Error().Msgf("Failed to get Git hooks path: %s\nOutput: %s\n", err, out)
|
|
}
|
|
}
|
|
|
|
hooksPath := strings.TrimSpace(string(out))
|
|
return hooksPath, nil
|
|
}
|
|
|
|
func createOrUpdatePreCommitFile(hooksPath string) error {
|
|
// File doesn't exist, create a new one
|
|
rootGitRepoPath, err := GetGitRoot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
filePath := fmt.Sprintf("%s/%s/%s", rootGitRepoPath, hooksPath, preCommitFile)
|
|
|
|
_, err = os.Stat(filePath)
|
|
if err == nil {
|
|
// File already exists, check if it contains the managed comments
|
|
content, err := ioutil.ReadFile(filePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read pre-commit file: %s", err)
|
|
}
|
|
|
|
if strings.Contains(string(content), "# MANAGED BY INFISICAL CLI (Do not modify): START") &&
|
|
strings.Contains(string(content), "# MANAGED BY INFISICAL CLI (Do not modify): END") {
|
|
return nil
|
|
}
|
|
|
|
// File already exists, append the template content
|
|
file, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY, 0755)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open pre-commit file: %s", err)
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
_, err = file.Write(preCommitTemplateAppend)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to append to pre-commit file: %s", err)
|
|
}
|
|
|
|
} else if os.IsNotExist(err) {
|
|
err = os.WriteFile(filePath, preCommitTemplate, 0755)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create pre-commit file: %s", err)
|
|
}
|
|
} else {
|
|
// Error occurred while checking file status
|
|
return fmt.Errorf("failed to check pre-commit file status: %s", err)
|
|
}
|
|
|
|
return nil
|
|
}
|