`push` runs `git add --all .`, so anything lying in the project gets committed, and with `mirror = true` it reaches a public server in the same breath. It is the one action in mgsh that cannot be undone: a deleted server repository comes back from an archive, a published credential does not. The staged diff is now scanned before the commit is made -- private keys, GitHub/GitLab/Slack/AWS/PyPI tokens, and credential-shaped assignments -- and a hit is shown with file and line before asking whether to continue. Declining leaves the changes staged but uncommitted, so removing the file and adding a .gitignore entry is all it takes. The hard part is not detection but silence. A scanner that cries wolf gets answered with a reflexive "y" and stops being a safety net, so values that are plainly environment references, dotted identifiers, constant names, template slots or masked stand-ins are filtered out. A test scans mgsh's own README and mgshrc.example -- both full of credential-shaped text -- and fails if either would trip the check. It caught the documentation for this very feature, which is why the README describes the sample output instead of reproducing it. For a line that legitimately looks like a credential there is `mgsh:allow`, which suppresses that one line; `secretscan = off` turns the check off entirely. Only an explicit "off" does that -- a typo in the setting leaves the safety net in place, which is what the new falsy() is for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
106 lines
2.0 KiB
Go
106 lines
2.0 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
func word(words []string, i int) string {
|
|
if i < len(words) {
|
|
return words[i]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// splitLines splits s on newlines, dropping a trailing newline and returning
|
|
// nil for empty input.
|
|
func splitLines(s string) []string {
|
|
s = strings.TrimRight(s, "\n")
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return strings.Split(s, "\n")
|
|
}
|
|
|
|
func isDir(p string) bool {
|
|
fi, err := os.Stat(p)
|
|
return err == nil && fi.IsDir()
|
|
}
|
|
|
|
// truthy reports whether a config string means "on" (1/true/yes/on).
|
|
func truthy(s string) bool {
|
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
|
case "1", "true", "yes", "on":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// falsy reports whether a config string explicitly means "off". It is not the
|
|
// negation of truthy: for a setting that defaults to on, an unset value or a
|
|
// typo must leave it on, and only a deliberate "off" may switch it off.
|
|
func falsy(s string) bool {
|
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
|
case "0", "false", "no", "off":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func fileExists(p string) bool {
|
|
fi, err := os.Stat(p)
|
|
return err == nil && !fi.IsDir()
|
|
}
|
|
|
|
func filesEqual(a, b string) bool {
|
|
da, ea := os.ReadFile(a)
|
|
db, eb := os.ReadFile(b)
|
|
if ea != nil || eb != nil {
|
|
return false
|
|
}
|
|
return bytes.Equal(da, db)
|
|
}
|
|
|
|
func copyFile(src, dst string) error {
|
|
data, err := os.ReadFile(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
|
return err
|
|
}
|
|
mode := os.FileMode(0644)
|
|
if fi, err := os.Stat(src); err == nil {
|
|
mode = fi.Mode()
|
|
}
|
|
return os.WriteFile(dst, data, mode)
|
|
}
|
|
|
|
func resolveIP() string {
|
|
h, err := os.Hostname()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
addrs, err := net.LookupHost(h)
|
|
if err != nil || len(addrs) == 0 {
|
|
return ""
|
|
}
|
|
for _, a := range addrs {
|
|
if ip := net.ParseIP(a); ip != nil && ip.To4() != nil {
|
|
return a
|
|
}
|
|
}
|
|
return addrs[0]
|
|
}
|
|
|
|
func shortHostname() string {
|
|
h, _ := os.Hostname()
|
|
if i := strings.IndexByte(h, '.'); i >= 0 {
|
|
h = h[:i]
|
|
}
|
|
return h
|
|
}
|