support filtering
This commit is contained in:
parent
cd4d796a7c
commit
6f019acd7b
|
@ -99,6 +99,16 @@ func defaultWatchListPathIfExists() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func defaultKeyListPath() string {
|
||||||
|
return filepath.Join(defaultConfigDir(), "keylist")
|
||||||
|
}
|
||||||
|
func defaultKeyListPathIfExists() string {
|
||||||
|
if fileExists(defaultKeyListPath()) {
|
||||||
|
return defaultKeyListPath()
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
func defaultScriptDir() string {
|
func defaultScriptDir() string {
|
||||||
return filepath.Join(defaultConfigDir(), "hooks.d")
|
return filepath.Join(defaultConfigDir(), "hooks.d")
|
||||||
}
|
}
|
||||||
|
@ -124,6 +134,15 @@ func readWatchListFile(filename string) (monitor.WatchList, error) {
|
||||||
return monitor.ReadWatchList(file)
|
return monitor.ReadWatchList(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readKeyListFile(filename string) (monitor.KeyList, error) {
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, simplifyError(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
return monitor.ReadKeyList(file)
|
||||||
|
}
|
||||||
|
|
||||||
func readEmailFile(filename string) ([]string, error) {
|
func readEmailFile(filename string) ([]string, error) {
|
||||||
file, err := os.Open(filename)
|
file, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -166,6 +185,7 @@ func main() {
|
||||||
verbose bool
|
verbose bool
|
||||||
version bool
|
version bool
|
||||||
watchlist string
|
watchlist string
|
||||||
|
keylist string
|
||||||
}
|
}
|
||||||
flag.IntVar(&flags.batchSize, "batch_size", 1000, "Max number of entries to request per call to get-entries (advanced)")
|
flag.IntVar(&flags.batchSize, "batch_size", 1000, "Max number of entries to request per call to get-entries (advanced)")
|
||||||
flag.Func("email", "Email address to contact when matching certificate is discovered (repeatable)", appendFunc(&flags.email))
|
flag.Func("email", "Email address to contact when matching certificate is discovered (repeatable)", appendFunc(&flags.email))
|
||||||
|
@ -179,6 +199,7 @@ func main() {
|
||||||
flag.BoolVar(&flags.verbose, "verbose", false, "Be verbose")
|
flag.BoolVar(&flags.verbose, "verbose", false, "Be verbose")
|
||||||
flag.BoolVar(&flags.version, "version", false, "Print version and exit")
|
flag.BoolVar(&flags.version, "version", false, "Print version and exit")
|
||||||
flag.StringVar(&flags.watchlist, "watchlist", defaultWatchListPathIfExists(), "File containing domain names to watch")
|
flag.StringVar(&flags.watchlist, "watchlist", defaultWatchListPathIfExists(), "File containing domain names to watch")
|
||||||
|
flag.StringVar(&flags.keylist, "keylist", defaultKeyListPathIfExists(), "File containing known key information")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if flags.version {
|
if flags.version {
|
||||||
|
@ -242,6 +263,15 @@ func main() {
|
||||||
config.WatchList = watchlist
|
config.WatchList = watchlist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if flags.keylist != "" {
|
||||||
|
keylist, err := readKeyListFile(flags.keylist)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s: error reading keylist from %q: %s\n", programName, flags.keylist, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
config.KeyList = keylist
|
||||||
|
}
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,11 @@ You can use Cert Spotter to detect:
|
||||||
certspotter reads the watch list only when starting up, so you must restart
|
certspotter reads the watch list only when starting up, so you must restart
|
||||||
certspotter if you change it.
|
certspotter if you change it.
|
||||||
|
|
||||||
|
-keylist *PATH*
|
||||||
|
|
||||||
|
: File containing known key information, one per line. A line consist of a
|
||||||
|
identifier and the sha256 of the public key separated by semi colon
|
||||||
|
|
||||||
# NOTIFICATIONS
|
# NOTIFICATIONS
|
||||||
|
|
||||||
When certspotter detects a certificate matching your watchlist, or encounters
|
When certspotter detects a certificate matching your watchlist, or encounters
|
||||||
|
|
|
@ -18,6 +18,7 @@ type Config struct {
|
||||||
State StateProvider
|
State StateProvider
|
||||||
StartAtEnd bool
|
StartAtEnd bool
|
||||||
WatchList WatchList
|
WatchList WatchList
|
||||||
|
KeyList KeyList
|
||||||
Verbose bool
|
Verbose bool
|
||||||
HealthCheckInterval time.Duration
|
HealthCheckInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
|
|
||||||
type DiscoveredCert struct {
|
type DiscoveredCert struct {
|
||||||
WatchItem WatchItem
|
WatchItem WatchItem
|
||||||
|
KeyItem KeyItem
|
||||||
LogEntry *LogEntry
|
LogEntry *LogEntry
|
||||||
Info *certspotter.CertInfo
|
Info *certspotter.CertInfo
|
||||||
Chain []ct.ASN1Cert // first entry is the leaf certificate or precertificate
|
Chain []ct.ASN1Cert // first entry is the leaf certificate or precertificate
|
||||||
|
@ -172,5 +173,9 @@ func certNotificationText(cert *DiscoveredCert, paths *certPaths) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func certNotificationSummary(cert *DiscoveredCert) string {
|
func certNotificationSummary(cert *DiscoveredCert) string {
|
||||||
return fmt.Sprintf("Certificate Discovered for %s", cert.WatchItem)
|
keytext := ""
|
||||||
|
if cert.KeyItem.String() != "" {
|
||||||
|
keytext = fmt.Sprintf(" (known as %s)", cert.KeyItem.String())
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Certificate Discovered for %s%s", cert.WatchItem, keytext)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
// Copyright (C) 2016, 2023 Opsmate, Inc.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla
|
||||||
|
// Public License, v. 2.0. If a copy of the MPL was not distributed
|
||||||
|
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
|
||||||
|
// See the Mozilla Public License for details.
|
||||||
|
|
||||||
|
package monitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/hex"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"software.sslmate.com/src/certspotter"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KeyItem struct {
|
||||||
|
domain string
|
||||||
|
keyinfo string
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeyList []KeyItem
|
||||||
|
|
||||||
|
func ParseKeyItem(str string) (KeyItem, error) {
|
||||||
|
fields := strings.Split(str, ";")
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return KeyItem{}, fmt.Errorf("empty domain")
|
||||||
|
}
|
||||||
|
if len(fields) == 1 {
|
||||||
|
return KeyItem{}, fmt.Errorf("empty key info")
|
||||||
|
}
|
||||||
|
return KeyItem{
|
||||||
|
domain: fields[0],
|
||||||
|
keyinfo: fields[1],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadKeyList(reader io.Reader) (KeyList, error) {
|
||||||
|
items := make(KeyList, 0, 50)
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
lineNo := 0
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
lineNo++
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item, err := ParseKeyItem(line)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w on line %d", err, lineNo)
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
return items, scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (item KeyItem) String() string {
|
||||||
|
return item.domain
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list KeyList) Matches(certInfo *certspotter.CertInfo) (bool, KeyItem) {
|
||||||
|
|
||||||
|
for _, item := range list {
|
||||||
|
// better would be to convert the input and compare the bytes (no uppercase/lower-case issue)
|
||||||
|
pub := sha256.Sum256(certInfo.TBS.PublicKey.FullBytes)
|
||||||
|
sum := hex.EncodeToString(pub[:])
|
||||||
|
if sum == item.keyinfo {
|
||||||
|
return true, item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, KeyItem{}
|
||||||
|
}
|
|
@ -91,11 +91,13 @@ func processCertificate(ctx context.Context, config *Config, entry *LogEntry, ce
|
||||||
if !matched {
|
if !matched {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
matched, keyItem := config.KeyList.Matches(certInfo)
|
||||||
|
|
||||||
cert := &DiscoveredCert{
|
cert := &DiscoveredCert{
|
||||||
WatchItem: watchItem,
|
WatchItem: watchItem,
|
||||||
LogEntry: entry,
|
LogEntry: entry,
|
||||||
Info: certInfo,
|
Info: certInfo,
|
||||||
|
KeyItem: keyItem,
|
||||||
Chain: chain,
|
Chain: chain,
|
||||||
TBSSHA256: sha256.Sum256(certInfo.TBS.Raw),
|
TBSSHA256: sha256.Sum256(certInfo.TBS.Raw),
|
||||||
SHA256: sha256.Sum256(chain[0]),
|
SHA256: sha256.Sum256(chain[0]),
|
||||||
|
|
Loading…
Reference in New Issue