pax_global_header00006660000000000000000000000064142235230070014510gustar00rootroot0000000000000052 comment=bbbff75d55a604643b62a5fe2ac32cb0bb2c446b dmarc-cat-0.15.0/000077500000000000000000000000001422352300700134265ustar00rootroot00000000000000dmarc-cat-0.15.0/.gitignore000066400000000000000000000004161422352300700154170ustar00rootroot00000000000000# Created by .ignore support plugin (hsz.mobi) ### Go template # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # goland .idea dmarc-cat-0.15.0/.travis.yml000066400000000000000000000006631422352300700155440ustar00rootroot00000000000000language: go arch: - amd64 - ppc64le go: - "1.15.x" - "1.18.x" - master matrix: allow_failures: - go: master fast_finish: true branches: only: - master - develop env: - GO111MODULE=on CGO_CFLAGS=-I/usr/local/include CGO_LDFLAGS=-L/usr/local/lib before_install: - sudo apt-get update -qq - sudo apt-get install -qq libgpgme11 libgpgme11-dev libassuan-dev libassuan0 libgpg-error0 gnupg2 script: - make - make test dmarc-cat-0.15.0/LICENSE.md000066400000000000000000000027421422352300700150370ustar00rootroot00000000000000Copyright (c) 2018, Ollivier Robert All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of the author. dmarc-cat-0.15.0/Makefile000066400000000000000000000007371422352300700150750ustar00rootroot00000000000000# Main Makefile for dmarc-cat # # Copyright 2018 © by Ollivier Robert # GO= go GOBIN= ${GOPATH}/bin BIN= dmarc-cat SRCS= analyze.go file.go main.go resolve.go types.go utils.go OPTS= -ldflags="-s -w" -v all: ${BIN} ${BIN}: ${SRCS} ${GO} build -o ${BIN} ${OPTS} . windows: ${SRCS} ${GO} build -o ${BIN}.exe ${OPTS} . test: ${GO} test -v . lint: gometalinter install: ${BIN} ${GO} install ${OPTS} . clean: ${GO} clean -v push: git push --all git push --tags dmarc-cat-0.15.0/README.md000066400000000000000000000101061422352300700147030ustar00rootroot00000000000000# README.md ## Status [![GitHub release](https://img.shields.io/github/release/keltia/dmarc-cat.svg)](https://github.com/keltia/dmarc-cat/releases/) [![GitHub issues](https://img.shields.io/github/issues/keltia/dmarc-cat.svg)](https://github.com/keltia/dmarc-cat/issues) [![Go Version](https://img.shields.io/badge/go-1.10-blue.svg)](https://golang.org/dl/) [![Build Status](https://travis-ci.org/keltia/dmarc-cat.svg?branch=master)](https://travis-ci.org/keltia/dmarc-cat) [![GoDoc](http://godoc.org/github.com/keltia/dmarc-cat?status.svg)](http://godoc.org/github.com/keltia/dmarc-cat) [![SemVer](https://img.shields.io/badge/semver-2.0.0-blue)](https://semver.org/spec/v2.0.0.html) [![License](https://img.shields.io/badge/License-BSD-blue)](https://opensource.org/licenses/BSD-2-Clause) [![Go Report Card](https://goreportcard.com/badge/github.com/keltia/dmarc-cat)](https://goreportcard.com/report/github.com/keltia/dmarc-cat) ## Summary `dmarc-cat` is a small command-line utility to analyze and display in a usable manner the content of the DMARC XML reports sent by the various email providers around the globe. Should work properly on UNIX (FreeBSD, Linux, etc.) and now Windows systems. ## Installation As with many Go utilities, a simple go get github.com/keltia/dmarc-cat is enough to fetch, build and install. On some systems you may need to add some environment variables to enable the Go and C compilers to find the `gpgme` include files and libraries. CGO_CFLAGS="-I/usr/local/include" CGO_LDFLAGS="-L/usr/local/lib" go get ... On Windows systems, GPG support is disabled in the `archive` module so you don't need to compile any non-Go code and the above `go get` command should work directly in a Powershell window. ## Dependencies Aside from the standard library, I use `github.com/intel/tfortools` to generate tables. go get -u github.com/intel/tfortools It also use my own module `github.com/keltia/archive` to handle the various archive types. If you use Go modules, it should all work automatically. ## Usage SYNOPSIS ``` dmarc-cat -hvDN [-j N] [-t type] [-S sort] [-version] Usage of ./dmarc-cat: -D Debug mode -N Do not resolve IPs -S string Sort results (default "\"Count\" \"dsc\"") -j int Parallel jobs (default 8) -t string File type for stdin mode -v Verbose mode -version Display version Example: $ dmarc-cat /tmp/yahoo.com\!keltia.net\!1518912000\!1518998399.xml Reporting by: Yahoo! Inc. — postmaster@dmarc.yahoo.com From 2018-02-18 01:00:00 +0100 CET to 2018-02-19 00:59:59 +0100 CET Domain: keltia.net Policy: p=none; dkim=r; spf=r Reports(1): IP Count From RFrom RDKIM RSPF 88.191.250.24 1 keltia.net keltia.net neutral pass ``` ## Columns The full XML grammar is available [here](https://tools.ietf.org/html/rfc7489#appendix-C) The report has several columns: - `IP` is matching IP address - `Count` is the number of times this IP was present - `From` is the `From:` header value - `RFrom` is the envelope `From` value - `RDKIM` is the result from DKIM checking - `RSPF` is the result from SPF checking ## Supported formats The file sent by MTAs can differ in format, some providers send zip files with both csv and XML files, some directly send compressed XML files. The `archive` module should support all these, please open an issue if not. ## Tests Getting close to 90% coverage. ## License This is released under the BSD 2-Clause license. See `LICENSE.md`. ## References - [DMARC](https://dmarc.org/) - [SPF](http://www.rfc-editor.org/info/rfc7208) - [DKIM](http://www.rfc-editor.org/info/rfc6376) - [archive](https://github.com/keltia/archive/) ## Contributing I use Git Flow for this package so please use something similar or the usual github workflow. 1. Fork it ( https://github.com/keltia/dmarc-cat/fork ) 2. Checkout the develop branch (`git checkout develop`) 3. Create your feature branch (`git checkout -b my-new-feature`) 4. Commit your changes (`git commit -am 'Add some feature'`) 5. Push to the branch (`git push origin my-new-feature`) 6. Create a new Pull Request dmarc-cat-0.15.0/analyze.go000066400000000000000000000075241422352300700154300ustar00rootroot00000000000000package main import ( "bytes" "fmt" "sync" "text/template" "time" "github.com/intel/tfortools" "github.com/pkg/errors" ) const ( reportTmpl = `{{.MyName}} {{.MyVersion}}/j{{.Jobs}} by {{.Author}} Reporting by: {{.Org}} — {{.Email}} From {{.DateBegin}} to {{.DateEnd}} Domain: {{.Domain}} Policy: p={{.Disposition}}; dkim={{.DKIM}}; spf={{.SPF}} Reports({{.Count}}): ` rowTmpl = `{{ table (sort . %s)}}` ) // My template vars type headVars struct { MyName string MyVersion string Jobs string Author string Org string Email string DateBegin string DateEnd string Domain string Disposition string DKIM string SPF string Pct int Count int } // Entry representes a single entry type Entry struct { IP string Count int From string RFrom string RDKIM string RSPF string } type IP struct { IP string Name string } // ParallelSolve is doing the IP to name resolution with a worker set. // XXX use Mutex func ParallelSolve(ctx *Context, iplist []IP) []IP { verbose("ParallelSolve with %d workers", ctx.jobs) var lock sync.Mutex wg := &sync.WaitGroup{} queue := make(chan IP, ctx.jobs) resolved := iplist ind := 0 for i := 0; i < ctx.jobs; i++ { wg.Add(1) debug("setting up w%d", i) go func(n int, wg *sync.WaitGroup) { defer wg.Done() var name string for e := range queue { ips, err := ctx.r.LookupAddr(e.IP) debug("ips=%#v", ips) if err != nil { name = e.IP } else { name = ips[0] } lock.Lock() resolved[ind].Name = name ind++ lock.Unlock() debug("w%d - ip=%s - name=%s", n, e.IP, name) } }(i, wg) } for _, q := range iplist { queue <- q } close(queue) wg.Wait() debug("resolved=%#v", resolved) return resolved } // GatherRows extracts all IP and return the rows func GatherRows(ctx *Context, r Feedback) []Entry { var ( rows []Entry iplist []IP newlist []IP ) ipslen := len(r.Records) verbose("Resolving all %d IPs", ipslen) iplist = make([]IP, ipslen) // Get all IPs for i, report := range r.Records { iplist[i] = IP{IP: report.Row.SourceIP.String()} } // Now we have a nice array newlist = ParallelSolve(ctx, iplist) verbose("Resolved %d IPs", ipslen) for i, report := range r.Records { ip0 := newlist[i].Name current := Entry{ IP: ip0, Count: report.Row.Count, From: report.Identifiers.HeaderFrom, RSPF: report.AuthResults.SPF.Result, RDKIM: report.AuthResults.DKIM.Result, } if report.AuthResults.DKIM.Domain == "" { current.RFrom = report.AuthResults.SPF.Domain } else { current.RFrom = report.AuthResults.DKIM.Domain } rows = append(rows, current) } return rows } // Analyze extract and display what we want func Analyze(ctx *Context, r Feedback) (string, error) { var buf bytes.Buffer tmplvars := &headVars{ MyName: MyName, MyVersion: MyVersion, Jobs: fmt.Sprintf("%d", fJobs), Author: Author, Org: r.Metadata.OrgName, Email: r.Metadata.Email, DateBegin: time.Unix(r.Metadata.Date.Begin, 0).String(), DateEnd: time.Unix(r.Metadata.Date.End, 0).String(), Domain: r.Policy.Domain, Disposition: r.Policy.P, DKIM: r.Policy.ADKIM, SPF: r.Policy.ASPF, Pct: r.Policy.Pct, Count: len(r.Records), } rows := GatherRows(ctx, r) if len(rows) == 0 { return "", fmt.Errorf("empty report") } // Header t := template.Must(template.New("r").Parse(reportTmpl)) err := t.ExecuteTemplate(&buf, "r", tmplvars) if err != nil { return "", errors.Wrapf(err, "error in template 'r'") } // Generate our template sortTmpl := fmt.Sprintf(rowTmpl, fSort) err = tfortools.OutputToTemplate(&buf, "reports", sortTmpl, rows, nil) if err != nil { return "", errors.Wrapf(err, "error in template 'reports'") } return buf.String(), nil } dmarc-cat-0.15.0/analyze_test.go000066400000000000000000000027751422352300700164720ustar00rootroot00000000000000package main import ( "encoding/xml" "fmt" "testing" "github.com/keltia/archive" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestAnalyze(t *testing.T) { ctx := &Context{NullResolver{}, 1} s, err := Analyze(ctx, Feedback{}) assert.Error(t, err) assert.Empty(t, s) } func TestGatherRows_Empty(t *testing.T) { ctx := &Context{NullResolver{}, 1} r := GatherRows(ctx, Feedback{}) assert.Empty(t, r) } func TestGatherRows_Good(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "testdata/example.com!keltia.net!1538604008!1538690408.xml" a, err := archive.New(file) require.NoError(t, err) body, err := a.Extract(".xml") require.NoError(t, err) var report Feedback err = xml.Unmarshal(body, &report) require.NoError(t, err) rows := GatherRows(ctx, report) assert.Equal(t, 1, len(rows)) } type ErrResolver struct{} func (ErrResolver) LookupAddr(ip string) ([]string, error) { return []string{"BAD"}, fmt.Errorf("fake error") } func TestParallelSolve_Error(t *testing.T) { ctx := &Context{r: ErrResolver{}, jobs: 1} td := []IP{ {IP: "8.8.8.8", Name: "BAD"}, {IP: "8.8.4.4", Name: "BAD"}, } ips := ParallelSolve(ctx, td) assert.NotEmpty(t, ips) assert.EqualValues(t, td, ips) } func TestParallelSolve_Good(t *testing.T) { ctx := &Context{r: NullResolver{}, jobs: 1} td := []IP{ {IP: "8.8.8.8", Name: "8.8.8.8"}, {IP: "8.8.4.4", Name: "8.8.4.4"}, } ips := ParallelSolve(ctx, td) assert.NotEmpty(t, ips) assert.EqualValues(t, td, ips) } dmarc-cat-0.15.0/dmarc.xsd000066400000000000000000000245661422352300700152510ustar00rootroot00000000000000 dmarc-cat-0.15.0/file.go000066400000000000000000000042721422352300700147010ustar00rootroot00000000000000package main import ( "bytes" "encoding/xml" "io" "io/ioutil" "path/filepath" "regexp" "github.com/keltia/archive" "github.com/pkg/errors" ) /* cf. https://tools.ietf.org/html/rfc7489#section-7.2.1.1 filename = receiver "!" policy-domain "!" begin-timestamp "!" end-timestamp [ "!" unique-id ] "." extension unique-id = 1*(ALPHA / DIGIT) */ const ( reFileName = `^([\S\.]+)!([\S\.]+)!([\d]+)!([\d]+)(![[:alnum:]]+)*(\.\S+)(\.(gz|zip))*$` ) var reFN *regexp.Regexp func init() { reFN = regexp.MustCompile(reFileName) } func checkFilename(file string) (ok bool) { base := filepath.Base(file) return reFN.MatchString(base) } // HandleZipFile is here for zip files because archive.NewFromReader() does not work here func HandleZipFile(ctx *Context, file string) (string, error) { debug("HandleZipFile") var body []byte a, err := archive.New(file) if err == nil { body, err = a.Extract(".xml") if err != nil { return "", errors.Wrap(err, "extract") } } else { // Got plain text (i.e. xml) if body, err = ioutil.ReadFile(file); err != nil { return "", errors.Wrap(err, "ReadFile") } } debug("xml=%s", string(body)) var report Feedback if err := xml.Unmarshal(body, &report); err != nil { return "", errors.Wrap(err, "unmarshall") } debug("report=%v\n", report) return Analyze(ctx, report) } // HandleSingleFile creates a tempdir and dispatch csv/zip files to handler. func HandleSingleFile(ctx *Context, r io.ReadCloser, typ int) (string, error) { debug("HandleSingleFile") var body []byte debug("typ=%d", typ) if typ == archive.ArchiveZip { return "", errors.New("unsupported") } a, err := archive.NewFromReader(r, typ) if err == nil { debug("a=%#v", a) body, err = a.Extract("") if err != nil { return "", errors.Wrap(err, "extract") } } else { // Got plain text (i.e. xml) buf := bytes.NewBuffer(body) _, err := io.Copy(buf, r) if err != nil { return "", errors.Wrap(err, "copy") } } debug("xml=%#v", body) var report Feedback if err := xml.Unmarshal(body, &report); err != nil { debug("%d %s", typ, fType) return "", errors.Wrap(err, "unmarshall") } debug("report=%v\n", report) return Analyze(ctx, report) } dmarc-cat-0.15.0/file_test.go000066400000000000000000000110601422352300700157310ustar00rootroot00000000000000package main import ( "os" "path/filepath" "testing" "github.com/keltia/archive" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCheckFilename(t *testing.T) { td := []struct { In string Out bool }{ {"foo.bar", false}, {"example.com!keltia.net!1538604008!1538690408.xml.gz", true}, {"example.com!keltia.net!1538604008!1538690408.xml.gz", true}, {"example.com!keltia.net!1538604008!1538690408.xml", true}, {"example.com!keltia.net!1538604008!1538690408!666.xml", true}, {"google.com!keltia.net!1538438400!1538524799.zip", true}, } for _, e := range td { res := checkFilename(e.In) assert.Equal(t, e.Out, res, e.In) } } func TestHandleZipFile(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "testdata/google.com!keltia.net!1538438400!1538524799.zip" txt, err := HandleZipFile(ctx, file) assert.NoError(t, err) assert.NotEmpty(t, txt) } func TestHandleZipFile_Xml(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "testdata/example.com!keltia.net!1538604008!1538690408.xml" txt, err := HandleZipFile(ctx, file) assert.NoError(t, err) assert.NotEmpty(t, txt) } func TestHandleZipFile_Bad(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "testdata/notempty.zip" txt, err := HandleZipFile(ctx, file) assert.Error(t, err) assert.Empty(t, txt) } func TestHandleZipFile_Bad1(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "testdata/bad.zip" txt, err := HandleZipFile(ctx, file) assert.Error(t, err) assert.Empty(t, txt) } func TestHandleZipFile_None(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "/nonexistent" txt, err := HandleZipFile(ctx, file) assert.Error(t, err) assert.Empty(t, txt) } func TestHandleSingleFile_Plain(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "testdata/empty.txt" fh, err := os.Open(file) require.NoError(t, err) txt, err := HandleSingleFile(ctx, fh, archive.Ext2Type(filepath.Ext(file))) assert.Error(t, err) assert.Empty(t, txt) } func TestHandleSingleFile_Gzip(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "testdata/example.com!keltia.net!1538604008!1538690408.xml.gz" fh, err := os.Open(file) require.NoError(t, err) txt, err := HandleSingleFile(ctx, fh, archive.Ext2Type(".gz")) assert.NoError(t, err) assert.NotEmpty(t, txt) } func TestHandleSingleFile_Zip(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "testdata/google.com!keltia.net!1538438400!1538524799.zip" fh, err := os.Open(file) require.NoError(t, err) txt, err := HandleSingleFile(ctx, fh, archive.Ext2Type(filepath.Ext(file))) assert.Error(t, err) assert.Empty(t, txt) } func TestHandleSingleFile_Xml(t *testing.T) { ctx := &Context{NullResolver{}, 1} fDebug = true file := "testdata/example.com!keltia.net!1538604008!1538690408.xml" fh, err := os.Open(file) require.NoError(t, err) assert.Equal(t, archive.ArchivePlain, archive.Ext2Type(filepath.Ext(file))) txt, err := HandleSingleFile(ctx, fh, archive.Ext2Type(filepath.Ext(file))) assert.NoError(t, err) assert.NotEmpty(t, txt) fDebug = false } func TestHandleSingleFile_Null(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "/dev/null" fh, err := os.Open(file) require.NoError(t, err) assert.Equal(t, archive.ArchivePlain, archive.Ext2Type(filepath.Ext(file))) txt, err := HandleSingleFile(ctx, fh, archive.Ext2Type(filepath.Ext(file))) assert.Error(t, err) assert.Empty(t, txt) } func TestHandleSingleFile_Txt(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "testdata/bad.xml" fh, err := os.Open(file) require.NoError(t, err) txt, err := HandleSingleFile(ctx, fh, 255) assert.Error(t, err) assert.Empty(t, txt) } func TestHandleSingleFile_TxtNull(t *testing.T) { ctx := &Context{NullResolver{}, 1} file := "/dev/null" fh, err := os.Open(file) require.NoError(t, err) txt, err := HandleSingleFile(ctx, fh, 255) assert.Error(t, err) assert.Empty(t, txt) } func TestHandleSingleFile_Verbose(t *testing.T) { fVerbose = true ctx := &Context{NullResolver{}, 1} file := "testdata/empty.txt" fh, err := os.Open(file) require.NoError(t, err) txt, err := HandleSingleFile(ctx, fh, archive.Ext2Type(filepath.Ext(file))) assert.Error(t, err) assert.Empty(t, txt) fVerbose = false } func TestHandleSingleFile_Debug(t *testing.T) { fDebug = true ctx := &Context{NullResolver{}, 1} file := "testdata/empty.txt" fh, err := os.Open(file) require.NoError(t, err) txt, err := HandleSingleFile(ctx, fh, archive.Ext2Type(filepath.Ext(file))) assert.Error(t, err) assert.Empty(t, txt) fDebug = false } dmarc-cat-0.15.0/go.mod000066400000000000000000000003001422352300700145250ustar00rootroot00000000000000module github.com/keltia/dmarc-cat require ( github.com/intel/tfortools v0.2.0 github.com/keltia/archive v0.9.1 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.3.0 ) go 1.13 dmarc-cat-0.15.0/go.sum000066400000000000000000000031441422352300700145630ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/intel/tfortools v0.2.0 h1:n8OWuJ2gysONk724KWpynicX/N0OrBNjbF/kGRduvQw= github.com/intel/tfortools v0.2.0/go.mod h1:VNwPPab3wzbXX9CBtgsD718qK1w6ryEydP6ZoIrbI7w= github.com/keltia/archive v0.9.1 h1:7DBasZh0gZeUAfPthwOiGKljF9MDSWkm0rqrsp7G/rs= github.com/keltia/archive v0.9.1/go.mod h1:ZYKhAaRolHUdi+vkeEDfWcp65XDOljDdxWq17j3I86A= github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I= github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/proglottis/gpgme v0.1.1 h1:72xI0pt/hy7pqsRxk32KExITkXp+RZErRizsA+up/lQ= github.com/proglottis/gpgme v0.1.1/go.mod h1:fPbW/EZ0LvwQtH8Hy7eixhp1eF3G39dtx7GUN+0Gmy0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= dmarc-cat-0.15.0/main.go000066400000000000000000000054261422352300700147100ustar00rootroot00000000000000package main import ( "flag" "fmt" "io" "log" "os" "path/filepath" "runtime" "strings" "github.com/keltia/archive" "github.com/pkg/errors" ) var ( // MyName is the application MyName = filepath.Base(os.Args[0]) // MyVersion is our version MyVersion = "0.15.0,parallel" // Author should be obvious Author = "Ollivier Robert" fDebug bool fJobs int fNoResolv bool fSort string fType string fVerbose bool fVersion bool ) // Context is passed around rather than being a global var/struct type Context struct { r Resolver jobs int } func init() { flag.BoolVar(&fDebug, "D", false, "Debug mode") flag.BoolVar(&fNoResolv, "N", false, "Do not resolve IPs") flag.IntVar(&fJobs, "j", runtime.NumCPU(), "Parallel jobs") flag.StringVar(&fSort, "S", `"Count" "dsc"`, "Sort results") flag.StringVar(&fType, "t", "", "File type for stdin mode") flag.BoolVar(&fVerbose, "v", false, "Verbose mode") flag.BoolVar(&fVersion, "version", false, "Display version") } func Version() { fmt.Printf("%s version %s/j%d archive/%s\n", MyName, MyVersion, fJobs, archive.Version()) } // Setup creates our context and check stuff func Setup(a []string) (*Context, error) { // Exist early if -version if fVersion { Version() return nil, nil } if fDebug { fVerbose = true debug("debug mode") } if len(a) < 1 { return nil, fmt.Errorf("You must specify at least one file.") } ctx := &Context{RealResolver{}, fJobs} // Make it easier to sub it out if fNoResolv { ctx.r = NullResolver{} } return ctx, nil } func SelectInput(file string) (io.ReadCloser, error) { debug("file=%s", file) if file == "-" { if fType == "" { return nil, errors.New("Wrong file type, use -t") } return os.Stdin, nil } // We have a filename if !checkFilename(file) { return nil, errors.New("bad filename") } // We want the full path myfile, err := filepath.Abs(file) if err != nil { return nil, errors.Wrapf(err, "Abs(%s)", file) } return os.Open(myfile) } func realmain(args []string) error { ctx, err := Setup(args) if ctx == nil { return errors.Wrap(err, "realmain") } var txt string // Look for input file or stdin/"-" file := args[0] verbose("Analyzing %s", file) var fType = filepath.Ext(file) if strings.ToLower(fType) == ".zip" { txt, err = HandleZipFile(ctx, file) if err != nil { return errors.Wrapf(err, "file %s:", file) } } else { in, err := SelectInput(file) if err != nil { return errors.Wrap(err, "SelectInput") } defer in.Close() typ := archive.Ext2Type(fType) txt, err = HandleSingleFile(ctx, in, typ) if err != nil { return errors.Wrapf(err, "file %s:", file) } } fmt.Println(txt) return nil } func main() { flag.Parse() if err := realmain(flag.Args()); err != nil { log.Fatalf("Error: %v\n", err) } } dmarc-cat-0.15.0/main_test.go000066400000000000000000000044451422352300700157470ustar00rootroot00000000000000package main import ( "os" "testing" "github.com/stretchr/testify/assert" ) func TestSetup(t *testing.T) { ctx, err := Setup([]string{}) assert.Nil(t, ctx) assert.Error(t, err) } func TestSetup2(t *testing.T) { ctx, err := Setup([]string{"foo.zip"}) assert.NotNil(t, ctx) assert.NoError(t, err) assert.IsType(t, (*Context)(nil), ctx) } func TestSetup3(t *testing.T) { fNoResolv = true ctx, err := Setup([]string{"foo.zip"}) assert.NotNil(t, ctx) assert.NoError(t, err) assert.IsType(t, (*Context)(nil), ctx) fNoResolv = false } func TestSetup4(t *testing.T) { fVersion = true ctx, err := Setup([]string{"foo.zip"}) assert.Nil(t, ctx) assert.NoError(t, err) fVersion = false } func TestVersion(t *testing.T) { Version() } func TestSelectInput_Bad(t *testing.T) { _, err := SelectInput("-") assert.Error(t, err) } func TestSelectInput_Good(t *testing.T) { fType = ".xml" r, err := SelectInput("-") assert.NoError(t, err) assert.EqualValues(t, os.Stdin, r) fType = "" } func TestSelectInput_Badfn(t *testing.T) { _, err := SelectInput("/testdata/google.com!keltia.net!1538438400!1538524798.xml") assert.Error(t, err) } // realmain() is the thing now func TestMain_Noargs(t *testing.T) { assert.Error(t, realmain([]string{})) } func TestMain_Noargs_Verbose(t *testing.T) { fVerbose = true assert.Error(t, realmain([]string{})) fVerbose = false } func TestMain_Noargs_Debug(t *testing.T) { fDebug = true assert.Error(t, realmain([]string{})) fDebug = false } func TestMain_Noargs_NoResolv(t *testing.T) { fNoResolv = true r := realmain([]string{"testdata/google.com!keltia.net!1538438400!1538524799.xml"}) fNoResolv = false assert.Empty(t, r) } func TestRealmain_GoodFile(t *testing.T) { r := realmain([]string{"testdata/google.com!keltia.net!1538438400!1538524799.xml"}) assert.Empty(t, r) } func TestRealmain_GoodFile1(t *testing.T) { r := realmain([]string{"testdata/google.com!keltia.net!1538438400!1538524799.zip"}) assert.Empty(t, r) } func TestRealmain_NoFile(t *testing.T) { r := realmain([]string{"/nonexistent"}) assert.NotEmpty(t, r) } func TestMain_EmptyArg(t *testing.T) { r := realmain([]string{"foo"}) assert.NotEmpty(t, r) } func TestMain_GoodFile(t *testing.T) { os.Args = append(os.Args, "testdata/google.com!keltia.net!1538438400!1538524799.zip") main() } dmarc-cat-0.15.0/resolve.go000066400000000000000000000010501422352300700154300ustar00rootroot00000000000000package main import ( "net" ) // Resolver is the main interface we use type Resolver interface { LookupAddr(addr string) ([]string, error) } // NullResolver is empty type NullResolver struct{} // LookupAddr always return a good and fixed answer func (NullResolver) LookupAddr(addr string) ([]string, error) { return []string{addr}, nil } // RealResolver will call the real one type RealResolver struct{} // LookupAddr use the real "net" function func (r RealResolver) LookupAddr(addr string) ([]string, error) { return net.LookupAddr(addr) } dmarc-cat-0.15.0/resolve_test.go000066400000000000000000000006751422352300700165030ustar00rootroot00000000000000package main import ( "testing" "github.com/stretchr/testify/assert" ) func TestNullResolver_LookupAddr(t *testing.T) { var r NullResolver resp, err := r.LookupAddr("example.com") assert.NoError(t, err) assert.Equal(t, []string{"example.com"}, resp) } func TestRealResolver_LookupAddr(t *testing.T) { var r RealResolver resp, err := r.LookupAddr("8.8.8.8") assert.NoError(t, err) assert.Equal(t, []string{"dns.google."}, resp) } dmarc-cat-0.15.0/testdata/000077500000000000000000000000001422352300700152375ustar00rootroot00000000000000dmarc-cat-0.15.0/testdata/bad.xml000066400000000000000000000000001422352300700164750ustar00rootroot00000000000000dmarc-cat-0.15.0/testdata/bad.zip000066400000000000000000000002441422352300700165110ustar00rootroot00000000000000PK ;mHMbad.xmlUT B[\ux PK ;mHMbad.xmlUTB[ux PKMAdmarc-cat-0.15.0/testdata/empty.txt000066400000000000000000000000001422352300700171240ustar00rootroot00000000000000dmarc-cat-0.15.0/testdata/example.com!keltia.net!1538604008!1538690408.xml000066400000000000000000000022411422352300700243310ustar00rootroot00000000000000 1.0 esa1.eurocontrol.c3s2.iphmx.com MAILER-DAEMON@esa1.eurocontrol.c3s2.iphmx.com 1a6ffb$9f77bea=6b2f4786a9824806@esa1.eurocontrol.c3s2.iphmx.com 1538604008 1538690408 keltia.net r r

none

100
88.191.250.24 1 none fail pass keltia.net keltia.net keltia.net mfrom pass
dmarc-cat-0.15.0/testdata/example.com!keltia.net!1538604008!1538690408.xml.gz000066400000000000000000000011211422352300700247440ustar00rootroot00000000000000u[esa1.eurocontrol.c3s2.iphmx.com!keltia.net!1538604008!1538690408.xmlT]o0 |i8@;`l2%Aߏl'Y #ϴӡm&{NjI6⺖jNuM&StB‚֗-xV3R(8'Y͵V7 pDm{HnI:2Ehl˷o/o?<߬/޲20TBS$\w/k՗X.+`E5e`b6/&NbitJԦ 6-||(< Fdȃcvĭ8K/j06EwxE(Y-$tFJ+ cgY7D{gplvpЙٝ,RZIʓc0&*םB1zgMnC"X Nqs LH0bL?3LFd K!m`Kau{ZNٗ^JoK k!noHQA =8d,G J%>tY4 dmarc-cat-0.15.0/testdata/google.com!keltia.net!1538438400!1538524799.xml000066400000000000000000000027611422352300700241730ustar00rootroot00000000000000 google.com noreply-dmarc-support@google.com https://support.google.com/a/answer/2466580 15591417298178277408 1538438400 1538524799 keltia.net r r

none

none 100
195.154.227.159 1 none fail fail keltia.net example.org pass 217.70.183.200 1 none fail fail keltia.net example.org pass
dmarc-cat-0.15.0/testdata/google.com!keltia.net!1538438400!1538524799.zip000066400000000000000000000013511422352300700241670ustar00rootroot00000000000000PKZFM@x/google.com!keltia.net!1538438400!1538524799.xmlUT *[*[ux TMs0Wx|72(JO홑ł5I#]񑤇{ 3o? r+X'zާq%t#Uǡ^m3WQ F[_y=vN뮇D聒|`gJ#C~hn&}߆M~1-V _Kjv޸od %p,z*#r}caiQ<-SUVdO+Ԗ.v:09b tZLF>-ٶCFRf<]`)DxB%OxdΛd:DЙTB%&~p3bgih+e$v귥G+"I<ɲ84; =*LGt^y?̚!Nz\X1RL$F2MrHsoT6l%^%[VK8壿qSԪ;`pn}>nu`;jO> γ#hIyL1†_ՓPKZFM@x/google.com!keltia.net!1538438400!1538524799.xmlUT*[ux PKu^dmarc-cat-0.15.0/testdata/notempty.txt000066400000000000000000000000171422352300700176550ustar00rootroot00000000000000this is a file dmarc-cat-0.15.0/testdata/notempty.txt.gz000066400000000000000000000000561422352300700202770ustar00rootroot00000000000000S[notempty.txt+,VD̜T.Pfdmarc-cat-0.15.0/testdata/notempty.zip000066400000000000000000000002751422352300700176460ustar00rootroot00000000000000PK ׉HMPf notempty.txtUT ft[z[ux this is a file PK ׉HMPf notempty.txtUTft[ux PKRUdmarc-cat-0.15.0/types.go000066400000000000000000000044321422352300700151240ustar00rootroot00000000000000package main import ( "net" ) // DateRange time period type DateRange struct { Begin int64 `xml:"begin"` End int64 `xml:"end"` } // ReportMetadata for the report type ReportMetadata struct { OrgName string `xml:"org_name"` Email string `xml:"email"` ExtraContactInfo string `xml:"extra_contact_info"` ReportID string `xml:"report_id"` Date DateRange `xml:"date_range"` Errors []string `xml:"error"` } // PolicyPublished found in DNS type PolicyPublished struct { Domain string `xml:"domain"` ADKIM string `xml:"adkim"` ASPF string `xml:"aspf"` P string `xml:"p"` SP string `xml:"sp"` Pct int `xml:"pct"` Fo string `xml:"fo"` } // PolicyEvaluated what was evaluated type PolicyEvaluated struct { Disposition string `xml:"disposition"` DKIM string `xml:"dkim"` SPF string `xml:"spf"` Reasons []PolicyOverrideReason `xml:"reason,omitempty"` } // PolicyOverrideReason are the reasons that may affect DMARC disposition // or execution thereof type PolicyOverrideReason struct { Type string `xml:"type"` Comment string `xml:"comment"` } // Row for each IP address type Row struct { SourceIP net.IP `xml:"source_ip"` Count int `xml:"count"` Policy PolicyEvaluated `xml:"policy_evaluated"` } // Identifiers headers checked type Identifiers struct { HeaderFrom string `xml:"header_from"` EnvelopeFrom string `xml:"envelope_from"` EnvelopeTo string `xml:"envelope_to,omitempty"` } // Result for each IP type Result struct { Domain string `xml:"domain"` Selector string `xml:"selector"` Result string `xml:"result"` HumanResult string `xml:"human_result"` } // AuthResults for DKIM/SPF type AuthResults struct { DKIM Result `xml:"dkim,omitempty"` SPF Result `xml:"spf,omitempty"` } // Record for each IP type Record struct { Row Row `xml:"row"` Identifiers Identifiers `xml:"identifiers"` AuthResults AuthResults `xml:"auth_results"` } // Feedback the report itself type Feedback struct { Version float32 `xml:"version"` Metadata ReportMetadata `xml:"report_metadata"` Policy PolicyPublished `xml:"policy_published"` Records []Record `xml:"record"` } dmarc-cat-0.15.0/utils.go000066400000000000000000000004441422352300700151170ustar00rootroot00000000000000package main import ( "log" ) // debug displays only if fDebug is set func debug(str string, a ...interface{}) { if fDebug { log.Printf(str, a...) } } // verbose displays only if fVerbose is set func verbose(str string, a ...interface{}) { if fVerbose { log.Printf(str, a...) } } dmarc-cat-0.15.0/utils_test.go000066400000000000000000000005001422352300700161470ustar00rootroot00000000000000package main import ( "testing" ) func TestVerbose_No(t *testing.T) { verbose("no") } func TestVerbose_Yes(t *testing.T) { fVerbose = true verbose("yes") fVerbose = false } func TestDebug_No(t *testing.T) { debug("no") } func TestDebug_Yes(t *testing.T) { fVerbose = true verbose("yes") fVerbose = false }