Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/release 1.11.0 #78

Merged
merged 16 commits into from
Jul 25, 2024
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
with:
go-version: ^1.22
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v5
- name: goreleaser-check
uses: goreleaser/goreleaser-action@v5
with:
Expand All @@ -43,8 +43,8 @@ jobs:
if: ${{ github.event_name == 'pull_request' }}
run: make test
- name: test-coverage
if: ${{ false && github.event_name == 'push' }}
uses: paambaati/codeclimate-action@v5.0.0
if: ${{ github.event_name == 'push' }}
uses: paambaati/codeclimate-action@v6.0.0
env:
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
with:
Expand Down
12 changes: 1 addition & 11 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,16 @@ linters:
enable-all: true
disable:
- gochecknoglobals
- exhaustivestruct
- nonamedreturns
- structcheck
- tagliatelle
- nosnakecase
- exhaustruct
- inamedparam
- exhaustive
- interfacer
- varnamelen
- scopelint
- deadcode
- depguard
- maligned
- varcheck
- intrange
- ifshort
- ireturn
- gofumpt
- golint
- gci

linters-settings:
Expand Down Expand Up @@ -69,9 +59,9 @@ issues:
- govet # fieldalignment issue, ignored for yaml readability
- path: ._test\.go
linters:
- goerr113
- gocritic
- errcheck
- maintidx
- err113
- funlen
- dupl
25 changes: 14 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,17 @@ Closest analogs, i can find, that not suit my needs very well:
- fast, scans ~470 containers with ~4000 connections in around 5 sec
- auto-clusterization based on graph topology
- deep inspection mode, in wich connections between procesess inside containers, also collected and shown
- unix-sockets connections
- 100% test-coverage

## known limitations

- only established and listen connections are listed (but script like [snapshots.sh](examples/snapshots.sh) can beat this)
- `composer-yaml` is not intended to be working out from the box, it can lack some of crucial information (even in `-full` mode),
or may contains cycles between nodes (removing `links` section in services may help), its main purpose is for system overview
- [gephi](https://github.com/gephi/gephi) fails to load edges from resulting graphviz, this can be fixed by any auto-replacement tool: `sed -i 's/->/ -> /g' myfile.dot`
- [gephi](https://github.com/gephi/gephi) fails to load edges from resulting graphviz, this can be fixed by any auto-replacement
tool: `sed -i 's/->/ -> /g' myfile.dot`
- unix-sockets works only in root mode on linux, this process involves inode matching to find correct connections

## installation

Expand All @@ -85,8 +88,6 @@ decompose [flags]
follow only this container by name(s), comma-separated or from @file
-format string
output format: csv, dot, json, puml, sdsl, stat, tree, yaml (default "json")
-full
extract full process info: (cmd, args, env) and volumes info
-help
show this help
-load value
Expand All @@ -97,10 +98,12 @@ decompose [flags]
json file with metadata for enrichment
-no-loops
remove connection loops (node to itself) from output
-no-orphans
remove orphaned (not connected) nodes from output
-out string
output: filename or "-" for stdout (default "-")
-proto string
protocol to scan: tcp, udp or all (default "all")
protocol to scan: tcp,udp,unix or all (default "all")
-silent
suppress progress messages in stderr
-skip-env string
Expand Down Expand Up @@ -129,8 +132,8 @@ type Item struct {
Labels map[string]string `json:"labels"`
} `json:"container"` // container info
Listen map[string][]{
Kind string `json:"kind"` // tcp / udp
Value int `json:"value"`
Kind string `json:"kind"` // tcp / udp / unix
Value string `json:"value"`
Local bool `json:"local"` // bound to loopback
} `json:"listen"` // ports with process names
Networks []string `json:"networks"` // network names
Expand Down Expand Up @@ -162,7 +165,7 @@ Single node example with full info and metadata filled:
"labels": {}
},
"listen": {"foo": [
{"kind": "tcp", "value": 80}
{"kind": "tcp", "value": "80"}
]},
"networks": ["test-net"],
"tags": ["some"],
Expand All @@ -180,7 +183,7 @@ Single node example with full info and metadata filled:
],
"connected": {
"bar-1": [
{"src": "foo", "dst": "[remote]", "port": {"kind": "tcp", "value": 443}}
{"src": "foo", "dst": "[remote]", "port": {"kind": "tcp", "value": "443"}}
]
}
}
Expand Down Expand Up @@ -261,7 +264,7 @@ a float in `(0.0, 1.0]` range, representing how much similar ports nodes must ha
Save full json stream:

```shell
decompose -full > nodes-1.json
sudo decompose > nodes-1.json
```

Get `dot` file:
Expand All @@ -270,10 +273,10 @@ Get `dot` file:
decompose -format dot > connections.dot
```

Get only tcp connections as `dot`:
Get tcp and udp connections as `dot`:

```shell
decompose -proto tcp -format dot > tcp.dot
decompose -proto tcp,udp -format dot > tcp.dot
```

Merge graphs from json streams, filter by protocol, skip remote hosts and save as `dot`:
Expand Down
63 changes: 39 additions & 24 deletions cmd/decompose/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ var (
)

var (
fSilent, fVersion bool
fHelp, fLocal bool
fFull, fNoLoops bool
fDeep, fCompress bool
fProto, fFormat string
fOut, fFollow string
fMeta, fCluster string
fSkipEnv string
fLoad []string
fSilent, fVersion bool
fHelp, fLocal bool
fNoLoops, fNoOrphans bool
fDeep, fCompress bool
fProto, fFormat string
fOut, fFollow string
fMeta, fCluster string
fSkipEnv string
fLoad []string

knownBuilders string
ErrUnknown = errors.New("unknown")
Expand Down Expand Up @@ -85,14 +85,14 @@ func setupFlags() {
flag.BoolVar(&fVersion, "version", false, "show version")
flag.BoolVar(&fHelp, "help", false, "show this help")
flag.BoolVar(&fLocal, "local", false, "skip external hosts")
flag.BoolVar(&fFull, "full", false, "extract full process info: (cmd, args, env) and volumes info")
flag.BoolVar(&fNoLoops, "no-loops", false, "remove connection loops (node to itself) from output")
flag.BoolVar(&fNoOrphans, "no-orphans", false, "remove orphaned (not connected) nodes from output")
flag.BoolVar(&fDeep, "deep", false, "process-based introspection")
flag.BoolVar(&fCompress, "compress", false, "compress graph")

flag.StringVar(&fOut, "out", defaultOutput, "output: filename or \"-\" for stdout")
flag.StringVar(&fMeta, "meta", "", "json file with metadata for enrichment")
flag.StringVar(&fProto, "proto", defaultProto, "protocol to scan: tcp, udp or all")
flag.StringVar(&fProto, "proto", defaultProto, "protocol to scan: tcp,udp,unix or all")
flag.StringVar(&fFollow, "follow", "", "follow only this container by name(s), comma-separated or from @file")
flag.StringVar(
&fCluster,
Expand Down Expand Up @@ -173,7 +173,7 @@ func makeClusterizer(
f, v string,
) (rv graph.NamedBuilderWriter, err error) {
if !builder.SupportCluster(f) {
log.Println(b.Name(), "cannot handle graph clusters - ignoring")
log.Println("[-]", b.Name(), "cannot handle graph clusters - ignoring")

return b, nil
}
Expand All @@ -191,7 +191,7 @@ func makeClusterizer(
high = 1.0
)

rv = cluster.NewLayers(b, min(high, max(low, simf)))
rv = cluster.NewLayers(b, min(high, max(low, simf)), "")
} else {
cr := cluster.NewRules(b, nil)

Expand Down Expand Up @@ -282,6 +282,12 @@ func prepareConfig() (
}
}

if fCompress {
cmp := graph.NewCompressor(bildr, "", defaultDiff, true)

bildr, nwr = cmp, cmp
}

if fCluster != "" {
cb, err := makeClusterizer(bildr, fFormat, fCluster)
if err != nil {
Expand All @@ -291,20 +297,16 @@ func prepareConfig() (
bildr, nwr = cb, cb
}

if fCompress {
cmp := graph.NewCompressor(bildr, defaultDiff, true)
if fNoOrphans {
cb := graph.NewOrphansInspector(bildr)

bildr, nwr = cmp, cmp
bildr, nwr = cb, cb
}

skipKeys := []string{}

if fSkipEnv != "" {
if fFull {
skipKeys = strings.Split(fSkipEnv, ",")
} else {
log.Println("skip-env makes no sense without full info - ignoring")
}
skipKeys = strings.Split(fSkipEnv, ",")
}

cfg = &graph.Config{
Expand All @@ -313,7 +315,6 @@ func prepareConfig() (
Proto: proto,
Follow: loadSet(fFollow),
OnlyLocal: fLocal,
FullInfo: fFull,
Deep: fDeep,
NoLoops: fNoLoops,
SkipEnv: skipKeys,
Expand Down Expand Up @@ -382,8 +383,15 @@ func doBuild(
mode := client.InContainer

if runtime.GOOS == linuxOS && os.Geteuid() == 0 {
opts = append(opts, client.WithNsEnter(client.Nsenter))
mode = client.LinuxNsenter
opts = append(opts,
client.WithNsenterFn(client.Nsenter),
client.WithInodesFn(client.Inodes),
)
} else if cfg.Proto.Has(graph.UNIX) {
log.Println("[-] Unix-connections requested in non-root mode, ignoring")

cfg.Proto ^= graph.UNIX
}

cli, err := client.NewDocker(append(opts, client.WithMode(mode))...)
Expand All @@ -393,7 +401,14 @@ func doBuild(

defer cli.Close()

log.Println("Starting with method:", cli.Mode())
method := cli.Mode()

if cfg.Deep {
method += " / deep"
}

log.Println("Starting with method:", method)
log.Println("Scanning for:", cfg.Proto.String())

if err = graph.Build(cfg, cli); err != nil {
return fmt.Errorf("graph: %w", err)
Expand Down
10 changes: 5 additions & 5 deletions examples/cluster.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@
},
{
"name": "ingress",
"if": "node.Listen.HasAny('80/tcp', '443/tcp')"
"if": "node.Listen.HasAny('tcp:80', 'tcp:443')"
},
{
"name": "backend",
"if": "node.Name startsWith 'back' && node.Listen.Has('8080/tcp', '8081/tcp')",
"if": "node.Name startsWith 'back' && node.Listen.Has('tcp:8080', 'tcp:8081')",
"weight": 2
},
{
"name": "store",
"if": "node.Listen.HasAny('3306/tcp', '5432/tcp')"
"if": "node.Listen.HasAny('tcp:3306', 'tcp:5432')"
},
{
"name": "redis",
"if": "node.Listen.Has('6379/tcp')"
"if": "node.Listen.Has('tcp:6379')"
},
{
"name": "queue",
"if": "node.Listen.HasAny('9092/tcp', '4222/tcp')"
"if": "node.Listen.HasAny('tcp:9092', 'tcp:4222')"
}
]
28 changes: 14 additions & 14 deletions examples/stream.json
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
{
"name": "nginx1",
"listen": {"nginx": [{"kind": "tcp", "value": 80}]},
"listen": {"nginx": [{"kind": "tcp", "value": "80"}]},
"connected": {
"back1": [{"src": "nginx", "dst": "app", "port": {"kind": "tcp", "value": 8080}}],
"back2": [{"src": "nginx", "dst": "app", "port": {"kind": "tcp", "value": 8081}}]
"back1": [{"src": "nginx", "dst": "app", "port": {"kind": "tcp", "value": "8080"}}],
"back2": [{"src": "nginx", "dst": "app", "port": {"kind": "tcp", "value": "8081"}}]
}
}
{
"name": "db1",
"listen": {"postgres": [{"kind": "tcp", "value": 5432}]},
"listen": {"postgres": [{"kind": "tcp", "value": "5432"}]},
"connected": {}
}
{
"name": "back1",
"listen": {"app": [
{"kind": "tcp", "value": 8080},
{"kind": "tcp", "value": 8081},
{"kind": "tcp", "value": 9000}
{"kind": "tcp", "value": "8080"},
{"kind": "tcp", "value": "8081"},
{"kind": "tcp", "value": "9000"}
]},
"connected": {
"db1": [{"src": "app", "dst": "postgres", "port": {"kind": "tcp", "value": 5432}}]
"db1": [{"src": "app", "dst": "postgres", "port": {"kind": "tcp", "value": "5432"}}]
}
}
{
"name": "back2",
"listen": {"app": [
{"kind": "tcp", "value": 8080},
{"kind": "tcp", "value": 8081}
{"kind": "tcp", "value": "8080"},
{"kind": "tcp", "value": "8081"}
]},
"connected": {
"db1": [{"src": "app", "dst": "postgres", "port": {"kind": "tcp", "value": 5432}}],
"foo1": [{"src": "app", "dst": "[remote]", "port": {"kind": "tcp", "value": 9500}}]
"db1": [{"src": "app", "dst": "postgres", "port": {"kind": "tcp", "value": "5432"}}],
"foo1": [{"src": "app", "dst": "[remote]", "port": {"kind": "tcp", "value": "9500"}}]
}
}
{
"name": "foo1",
"is_external": true,
"listen": {"[remote]": [{"kind": "tcp", "value": 9500}]},
"listen": {"[remote]": [{"kind": "tcp", "value": "9500"}]},
"connected": {
"back1": [
{"src": "[remote]", "dst": "app", "port": {"kind": "tcp", "value": 9000}}
{"src": "[remote]", "dst": "app", "port": {"kind": "tcp", "value": "9000"}}
]
}
}
Loading
Loading