Skip to content

Commit 5038ca3

Browse files
committed
Feat(ssh): credential prompting
1 parent 4583214 commit 5038ca3

File tree

6 files changed

+147
-51
lines changed

6 files changed

+147
-51
lines changed

config.go

Lines changed: 101 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,26 @@ import (
44
"flag"
55
"fmt"
66
"github.com/sandflysecurity/sandfly-entropyscan/pkg/ssh"
7+
"golang.org/x/term"
78
"log"
89
"os"
910
"sync"
1011
"time"
1112
)
1213

14+
type sshConfig struct {
15+
Host string
16+
User string
17+
Passwd string
18+
KeyFile string
19+
KeyFilePassphrase string
20+
Version string
21+
Port int
22+
Agent bool
23+
Prompt bool
24+
Timeout time.Duration
25+
}
26+
1327
type outputConfig struct {
1428
delimChar string
1529
csvOutput bool
@@ -18,17 +32,6 @@ type outputConfig struct {
1832
outputFile string
1933
}
2034

21-
type sshConfig struct {
22-
Host string
23-
User string
24-
Passwd string
25-
KeyFile string
26-
Version string
27-
Port int
28-
Agent bool
29-
Timeout time.Duration
30-
}
31-
3235
type inputConfig struct {
3336
filePath string
3437
dirPath string
@@ -60,6 +63,31 @@ type config struct {
6063

6164
var cfgOnce sync.Once
6265

66+
func (scfg *sshConfig) prompt() {
67+
ploop:
68+
for {
69+
switch {
70+
case scfg.Host == "":
71+
_, _ = fmt.Printf("Host: ")
72+
_, _ = fmt.Scanln(&scfg.Host)
73+
case scfg.User == "":
74+
_, _ = fmt.Printf("[%s] User: ", scfg.Host)
75+
_, _ = fmt.Scanln(&scfg.User)
76+
case scfg.KeyFile != "" && scfg.KeyFilePassphrase == "":
77+
_, _ = fmt.Printf("[%s] Pass: ", scfg.KeyFile)
78+
pb, _ := term.ReadPassword(int(os.Stdin.Fd()))
79+
scfg.KeyFilePassphrase = string(pb)
80+
case scfg.KeyFile == "" && scfg.Passwd == "":
81+
_, _ = fmt.Printf("[%s] Pass: ", scfg.Host)
82+
pb, _ := term.ReadPassword(int(os.Stdin.Fd()))
83+
scfg.Passwd = string(pb)
84+
default:
85+
break ploop
86+
}
87+
}
88+
print("\n")
89+
}
90+
6391
func (cfg *config) parseFlags() {
6492
sumMD5, sumSHA1, sumSHA256, sumSHA512 := true, true, true, true
6593

@@ -70,37 +98,74 @@ func (cfg *config) parseFlags() {
7098
&sumSHA512: HashTypeSHA512,
7199
}
72100

101+
// # Strings
102+
73103
flag.StringVar(&cfg.inCfg.filePath, "file", "", "full path to a single file to analyze")
74104
flag.StringVar(&cfg.inCfg.dirPath, "dir", "", "directory name to analyze")
75105
flag.StringVar(&cfg.outCfg.delimChar, "delim", constDelimeterDefault, "delimeter for CSV output")
76-
flag.StringVar(&cfg.outCfg.outputFile, "output", "", "output file to write results to (default stdout) (only json and csv formats supported)")
77-
78-
flag.Float64Var(&cfg.entropyMaxVal, "entropy", 0, "show any file with entropy greater than or equal to this value (0.0 - 8.0 max 8.0, default is 0)")
79-
80-
flag.BoolVar(&cfg.elfOnly, "elf", false, "only check ELF executables")
81-
flag.BoolVar(&cfg.procOnly, "proc", false, "check running processes")
82-
flag.BoolVar(&cfg.outCfg.csvOutput, "csv", false, "output results in CSV format (filename, path, entropy, elf_file [true|false], MD5, SHA1, SHA256, SHA512)")
83-
flag.BoolVar(&cfg.outCfg.jsonOutput, "json", false, "output results in JSON format")
84-
flag.BoolVar(&cfg.outCfg.printInterimResults, "print", false, "print interim results to stdout even if output file is specified")
85-
flag.BoolVar(&cfg.version, "version", false, "show version and exit")
86-
87-
flag.BoolVar(&sumMD5, "md5", true, "calculate and show MD5 checksum of file(s)")
88-
flag.BoolVar(&sumSHA1, "sha1", true, "calculate and show SHA1 checksum of file(s)")
89-
flag.BoolVar(&sumSHA256, "sha256", true, "calculate and show SHA256 checksum of file(s)")
90-
flag.BoolVar(&sumSHA512, "sha512", true, "calculate and show SHA512 checksum of file(s)")
106+
flag.StringVar(
107+
&cfg.outCfg.outputFile, "output", "",
108+
"output file to write results to (default stdout) (only json and csv formats supported)",
109+
)
110+
111+
// # Floats
112+
113+
flag.Float64Var(
114+
&cfg.entropyMaxVal, "entropy", 5.0,
115+
"show any file with entropy greater than or equal to this value (0.0 - 8.0, max 8.0) (def: 5.0)",
116+
)
117+
118+
// # Bools
119+
120+
flag.BoolVar(&cfg.elfOnly, "elf", true, "only check ELF executables (def: true)")
121+
flag.BoolVar(&cfg.procOnly, "proc", false, "check running processes (def: false)")
122+
flag.BoolVar(
123+
&cfg.outCfg.csvOutput, "csv", false,
124+
"output results in CSV format (def: false)\n"+
125+
"(filename, path, entropy, elf_file [true|false], MD5, SHA1, SHA256, SHA512)",
126+
)
127+
flag.BoolVar(&cfg.outCfg.jsonOutput, "json", false, "output results in JSON format (def: false)")
128+
flag.BoolVar(
129+
&cfg.outCfg.printInterimResults, "print", false,
130+
"print interim results to stdout even if output file is specified (def: false)",
131+
)
132+
flag.BoolVar(&cfg.version, "version", false, "show version and exit (def: false)")
133+
flag.BoolVar(&sumMD5, "md5", true, "calculate and show MD5 checksum of file(s) (def: true)")
134+
flag.BoolVar(&sumSHA1, "sha1", true, "calculate and show SHA1 checksum of file(s) (def: true)")
135+
flag.BoolVar(
136+
&sumSHA256, "sha256", true,
137+
"calculate and show SHA256 checksum of file(s) (def: true)",
138+
)
139+
flag.BoolVar(
140+
&sumSHA512, "sha512", true,
141+
"calculate and show SHA512 checksum of file(s) (def: true)",
142+
)
143+
flag.BoolVar(&cfg.ignoreSelf, "ignore-self", true, "ignore self process (def: true)")
144+
flag.BoolVar(
145+
&cfg.goFast, "fast", false,
146+
"use worker pool for concurrent file processing (experimental)",
147+
)
148+
149+
// # SSH
91150

92151
flag.StringVar(&cfg.inCfg.sshConfig.Host, "ssh-host", "", "SSH host to connect to")
93152
flag.StringVar(&cfg.inCfg.sshConfig.User, "ssh-user", "", "SSH user name")
94153
flag.StringVar(&cfg.inCfg.sshConfig.Passwd, "ssh-pass", "", "SSH password")
95154
flag.StringVar(&cfg.inCfg.sshConfig.KeyFile, "ssh-key", "", "SSH private key file")
155+
flag.StringVar(
156+
&cfg.inCfg.sshConfig.KeyFilePassphrase,
157+
"ssh-key-pass", "", "SSH private key passphrase",
158+
)
96159
flag.DurationVar(&cfg.inCfg.sshConfig.Timeout, "ssh-timeout", 30*time.Second, "SSH connection timeout")
97-
flag.StringVar(&cfg.inCfg.sshConfig.Version, "ssh-version", "SSH-2.0-EntropyScanner", "SSH version string")
160+
flag.StringVar(
161+
&cfg.inCfg.sshConfig.Version, "ssh-version", "SSH-2.0-SFScan", "SSH version string",
162+
)
98163
flag.IntVar(&cfg.inCfg.sshConfig.Port, "ssh-port", 22, "SSH port")
99164
flag.BoolVar(&cfg.inCfg.sshConfig.Agent, "ssh-agent", false, "use SSH agent")
100-
101-
flag.BoolVar(&cfg.goFast, "fast", false, "use worker pool for concurrent file processing (experimental)")
102-
103-
flag.BoolVar(&cfg.ignoreSelf, "ignore-self", true, "ignore self process")
165+
flag.BoolVar(
166+
&cfg.inCfg.sshConfig.Prompt, "ssh-prompt", false,
167+
"prompt for credentials (def: false)",
168+
)
104169

105170
flag.Parse()
106171

@@ -134,6 +199,10 @@ func newConfigFromFlags() *config {
134199
log.Fatal("only one of -file, -dir, or -ssh-host can be specified")
135200
}
136201

202+
if cfg.inCfg.sshConfig.Prompt {
203+
cfg.inCfg.sshConfig.prompt()
204+
}
205+
137206
if cfg.inCfg.sshConfig.Host != "" && cfg.inCfg.sshConfig.User == "" {
138207
log.Fatal("ssh-host requires ssh-user")
139208
}
@@ -146,6 +215,7 @@ func newConfigFromFlags() *config {
146215
!cfg.inCfg.sshConfig.Agent &&
147216
cfg.inCfg.sshConfig.KeyFile == "" &&
148217
cfg.inCfg.sshConfig.Passwd == "" {
218+
149219
log.Fatal("ssh mode requires ssh-key, ssh-pass, or ssh-agent")
150220
}
151221

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.19
55
require (
66
github.com/panjf2000/ants/v2 v2.9.1
77
golang.org/x/crypto v0.18.0
8+
golang.org/x/term v0.16.0
89
)
910

1011
require (

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
2121
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
2222
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
2323
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
24+
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
2425
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
2526
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
2627
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

pkg/ssh/auth.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,38 @@ func (s *SSH) WithPassword(password string) *SSH {
2020
}
2121

2222
// WithKey parses data from an SSH key to extract signers for authentication.
23-
func (s *SSH) WithKey(key []byte) *SSH {
24-
signer, err := ssh.ParsePrivateKey(key)
23+
func (s *SSH) WithKey(key []byte, pass ...string) *SSH {
24+
var err error
25+
var signer ssh.Signer
26+
27+
if pass == nil || len(pass) == 0 || pass[0] == "" {
28+
signer, err = ssh.ParsePrivateKey(key)
29+
} else {
30+
signer, err = ssh.ParsePrivateKeyWithPassphrase(key, []byte(pass[0]))
31+
}
32+
2533
if err != nil {
26-
_, _ = os.Stdout.WriteString(err.Error() + "\n")
34+
_, _ = os.Stderr.WriteString(err.Error() + "\n")
2735
return s
2836
}
37+
2938
s.auth = append(s.auth, ssh.PublicKeys(signer))
3039
return s
3140
}
3241

3342
// WithKeyFile parses data from an SSH to be processed by [s.WithKey].
34-
func (s *SSH) WithKeyFile(path string) *SSH {
43+
func (s *SSH) WithKeyFile(path string, pass ...string) *SSH {
3544
dat, err := os.ReadFile(path)
3645
if err != nil {
37-
_, _ = os.Stdout.WriteString(err.Error() + "\n")
46+
_, _ = os.Stderr.WriteString(err.Error() + "\n")
3847
return s
3948
}
40-
return s.WithKey(dat)
49+
return s.WithKey(dat, pass...)
50+
}
51+
52+
// WithEncryptedKeyFile parses data from an SSH key to extract signers for authentication.
53+
func (s *SSH) WithEncryptedKeyFile(path, pass string) *SSH {
54+
return s.WithKeyFile(path, pass)
4155
}
4256

4357
// WithAgent adds all available signers from an SSH agent to the [SSH] struct for authentication. (*nix)
@@ -51,7 +65,7 @@ func (s *SSH) WithAgent() *SSH {
5165
sshAgent := agent.NewClient(conn)
5266
signers, serr := sshAgent.Signers()
5367
if serr != nil {
54-
_, _ = os.Stdout.WriteString(serr.Error() + "\n")
68+
_, _ = os.Stderr.WriteString(serr.Error() + "\n")
5569
_ = conn.Close()
5670
return s
5771
}

pkg/ssh/ssh.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99

1010
const (
1111
defaultSSHPort = 22
12-
defaultSSHVersion = "SSH-2.0-EntropyScan"
12+
defaultSSHVersion = "SSH-2.0-SFScan"
1313
)
1414

1515
// SSH is a struct that enables using SSH for remote agent-less entropy scanning.
@@ -75,12 +75,15 @@ func (s *SSH) Connect() error {
7575
}
7676

7777
config := &ssh.ClientConfig{
78-
User: s.user,
79-
Auth: s.auth,
80-
ClientVersion: s.ver,
81-
Timeout: s.tout,
78+
User: s.user,
79+
Auth: s.auth,
80+
ClientVersion: s.ver,
81+
Timeout: s.tout,
82+
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
8283
}
8384

85+
config.SetDefaults()
86+
8487
var err error
8588
if s.client, err = ssh.Dial("tcp", s.host+":"+fmt.Sprintf("%d", s.port), config); err != nil {
8689
return err

scan.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,19 @@ func (cfg *config) scanSSH(parallel bool) error {
250250
errs = append(errs, perr)
251251
}
252252

253+
syncWork := func(pid int, pidPath string, pidData []byte) {
254+
perr := cfg.sshProcess(pid, pidPath, pidData)
255+
if errors.Is(perr, ErrNotElf) || errors.Is(perr, ErrLowEntropy) {
256+
log.Println(perr.Error())
257+
wg.Done()
258+
return
259+
}
260+
errMu.Lock()
261+
errs = append(errs, perr)
262+
errMu.Unlock()
263+
wg.Done()
264+
}
265+
253266
concurrent := func(pid int) {
254267
pidPath, pidData, err := cfg.sshPID(pid)
255268
if err != nil {
@@ -260,20 +273,14 @@ func (cfg *config) scanSSH(parallel bool) error {
260273
}
261274

262275
wg.Add(1)
263-
_ = workers.Submit(func() {
264-
perr := cfg.sshProcess(pid, pidPath, pidData)
265-
errMu.Lock()
266-
errs = append(errs, perr)
267-
errMu.Unlock()
268-
wg.Done()
269-
})
276+
277+
_ = workers.Submit(func() { syncWork(pid, pidPath, pidData) })
270278
}
271279

272280
for pid := constMinPID; pid < constMaxPID; pid++ {
273281
switch parallel {
274282
case false:
275283
synchronous(pid)
276-
continue
277284
case true:
278285
concurrent(pid)
279286
}

0 commit comments

Comments
 (0)