Skip to content

Commit 74eabf3

Browse files
authored
Merge pull request #13 from cloud-ecs/feat/virustotal/dcc
- feat: add dcc and virustotal support - prune scanner list to [mostly] enabled - overhaul web UI, zero deps, on brand - ci release - es2024 updates
2 parents 5382626 + b521b5b commit 74eabf3

24 files changed

Lines changed: 901 additions & 330 deletions

.github/workflows/release.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: release
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
paths:
7+
- package.json
8+
9+
env:
10+
CI: true
11+
12+
jobs:
13+
release:
14+
uses: haraka/.github/.github/workflows/release.yml@master
15+
secrets: inherit
16+
permissions:
17+
contents: write
18+
19+
publish:
20+
needs: release
21+
if: needs.release.outputs.changed == 'true'
22+
uses: haraka/.github/.github/workflows/publish.yml@master
23+
secrets: inherit
24+
permissions:
25+
contents: read
26+
packages: write
27+
id-token: write

README.md

Lines changed: 66 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
# Cloud Email Scanner Server
1+
# Email Content Scanning Daemon
22

33
[![Build][ci-img]][ci-url] [![Cover][cov-img]][cov-url] [![Qlty][qlty-img]][qlty-url]
44

55
A web service that receives emails via HTTP and scans them with one or many filtering engines (antivirus, antispam, policy engines, etc.)
66

77
## Content Scanners
88

9-
- [x] spamassassin
10-
- [x] rspamd
11-
- [ ] dcc
12-
- [ ] virustotal
13-
- [x] clamav
14-
- [x] bitdefender
15-
- [x] opendkim
16-
- [ ] opendmarc
9+
- [x] [spamassassin](https://spamassassin.apache.org)
10+
- [x] [rspamd](https://rspamd.com)
11+
- [x] [dcc](https://www.dcc-servers.net/dcc/)
12+
- [x] [virustotal](https://www.virustotal.com)
13+
- [x] [clamav](https://www.clamav.net)
14+
- [x] [bitdefender](https://www.bitdefender.com/business/antivirus.html)
15+
- [x] [opendkim](http://www.opendkim.org)
16+
- [ ] [opendmarc](http://www.opendmarc.org)
1717

18-
### Still viable?
18+
### Viable?
1919

2020
- [ ] fprot
2121
- [ ] f-secure
@@ -33,51 +33,73 @@ A web service that receives emails via HTTP and scans them with one or many filt
3333

3434
## Request
3535

36-
Send emails to be scanned as standard HTTP file upload.
36+
Send a message to be scanned as a standard HTTP file upload. The form field
37+
name is ignored — the first uploaded file is scanned.
3738

38-
`curl -X POST -F eicar=@eicar.eml localhost:8000/scan`
39-
40-
Sent metadata about the connection / message as HTTP headers.
41-
42-
`curl -X POST -F eicar=@eicar.eml -H 'X-GeoIP: AS, CN, Fuzhou' -H 'X-Remote-IP: 27.150.160.26' localhost:8000/scan`
39+
```sh
40+
curl -X POST -F upload=@test/files/eicar.eml localhost:8000/scan
41+
```
4342

4443
## Response
4544

46-
The response is a JSON encoded array, with the request and response for
47-
each available scanner.
45+
The response is a JSON array with one entry per available scanner. Each entry
46+
reports `pass`, `fail`, and `error` lists alongside the engine's `raw` output;
47+
a non-empty `fail` means that engine flagged the message.
48+
49+
The example below is a real scan of the bundled EICAR test message, with `raw`
50+
trimmed for brevity.
4851

4952
```json
5053
[
51-
{
52-
"name":"clamav",
53-
"pass":[],
54-
"fail":["Eicar-Test-Signature"],
55-
"error":[],
56-
"raw":"/Users/matt/Documents/git/ess/spool/upload_9f8c482aaaa10fcf501bf5259c00746c.eml: Eicar-Test-Signature FOUND\n"
57-
},
58-
{
59-
"name":"spamassassin",
60-
"pass":["ham"],
61-
"fail":[],
62-
"raw":"SPAMD/1.1 0
63-
EX_OK\r\nContent-length: 62\r\nSpam: False ; 2.3 /
64-
5.0\r\n\r\nAPOSTROPHE_FROM,MISSING_DATE,MISSING_MID,NO_RECEIVED,NO_RELAYS","error":[]},{"pass":["5646b98f634915112796250"],"fail":[],"name":"dspam","raw":"X-DSPAM-Result:
65-
matt; result=\"Innocent\"; class=\"Whitelisted\"; probability=0.0000; confidence=0.99; signature=5646b98f634915112796250\n",
66-
"error":[]
67-
},
68-
{
69-
"name":"opendkim",
70-
"pass":[],
71-
"fail":["message not signed"],
72-
"raw":"opendkim: /Users/matt/Documents/git/ess/spool/upload_9f8c482aaaa10fcf501bf5259c00746c.eml: message not signed\n",
73-
"error":[]
74-
}
54+
{
55+
"name": "clamav",
56+
"pass": [],
57+
"fail": ["Eicar-Signature"],
58+
"error": [],
59+
"raw": "stream: Eicar-Signature FOUND"
60+
},
61+
{
62+
"name": "virustotal",
63+
"pass": [],
64+
"fail": [41],
65+
"error": []
66+
},
67+
{
68+
"name": "spamassassin",
69+
"pass": ["ham"],
70+
"fail": [],
71+
"error": []
72+
},
73+
{
74+
"name": "rspamd",
75+
"pass": [],
76+
"fail": [8.5],
77+
"error": []
78+
},
79+
{
80+
"name": "dcc",
81+
"pass": ["A"],
82+
"fail": [],
83+
"error": [],
84+
"raw": "A\nA\nX-DCC-...; bulk rep Body=3 Fuz1=3 Fuz2=3 rep=23%\n\n"
85+
},
86+
{
87+
"name": "opendkim",
88+
"pass": [],
89+
"fail": ["message not signed"],
90+
"error": []
91+
}
7592
]
7693
```
7794

78-
### Status Page
95+
## Status
96+
97+
The daemon serves a small web UI (Home, Status, Scan) on the configured listen
98+
port, backed by two JSON endpoints:
7999

80-
![status page image](https://cloud.githubusercontent.com/assets/261635/11162087/56acf54a-8a46-11e5-882c-5d8b5a704d71.png)
100+
- `GET /status/scannersAll` — every known scanner
101+
- `GET /status/scannersAvailable` — scanners currently reachable, and the
102+
interface (`cli`, `socket`, or `network`) each was detected on
81103

82104
[ci-img]: https://github.com/cloud-ecs/ecsd/actions/workflows/ci.yml/badge.svg
83105
[ci-url]: https://github.com/cloud-ecs/ecsd/actions/workflows/ci.yml

cloud-email-scanner.ini

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ port = 8000
4848
; bin = rspamc
4949
; args =
5050

51+
[virustotal]
52+
; cloud API; requires a key (or set VIRUSTOTAL_API_KEY in the environment).
53+
; Scanning an unknown file uploads it to virustotal.com for analysis.
54+
; apikey =
55+
; timeout = 30
56+
5157
[avg]
5258
; interface = network | cli
5359

@@ -66,10 +72,15 @@ port = 8000
6672
; args =
6773

6874
[dcc]
69-
; interface = cli
75+
; interface = socket | network
76+
; timeout = 30
77+
; dccifd listener socket (DCC home dir default)
78+
; socket = /var/dcc/dccifd
7079

71-
[dcc.cli]
72-
; bin = dccifd
80+
[dcc.net]
81+
; dccifd has no standard TCP port; point this at your daemon
82+
; host = 0.0.0.0
83+
; port = 1025
7384

7485
[kaspersky]
7586

eslint.config.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ module.exports = [
1515
},
1616
rules: {
1717
'no-console': 'off',
18-
'no-unused-vars': 'warn',
18+
'no-unused-vars': [
19+
'warn',
20+
{ argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
21+
],
1922
},
2023
},
2124
]

html/css/ecsd.css

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
:root {
2+
--bg: #1b1e24;
3+
--panel: #252830;
4+
--fg: #e6e8eb;
5+
--muted: #9aa0a6;
6+
--accent: #4da3ff;
7+
--pass: #3fb950;
8+
--fail: #f85149;
9+
--border: #333842;
10+
}
11+
12+
* {
13+
box-sizing: border-box;
14+
}
15+
16+
body {
17+
margin: 0;
18+
font-family:
19+
system-ui,
20+
-apple-system,
21+
'Segoe UI',
22+
Roboto,
23+
Helvetica,
24+
Arial,
25+
sans-serif;
26+
line-height: 1.5;
27+
background-color: var(--bg);
28+
color: var(--fg);
29+
}
30+
31+
nav.navbar {
32+
display: flex;
33+
align-items: center;
34+
gap: 1.5rem;
35+
padding: 0.75rem 1.5rem;
36+
background-color: var(--panel);
37+
border-bottom: 1px solid var(--border);
38+
}
39+
40+
nav.navbar .brand {
41+
margin-right: auto;
42+
font-weight: 600;
43+
color: var(--fg);
44+
text-decoration: none;
45+
}
46+
47+
nav.navbar a {
48+
color: var(--muted);
49+
text-decoration: none;
50+
}
51+
52+
nav.navbar a:hover {
53+
color: var(--accent);
54+
}
55+
56+
main {
57+
max-width: 900px;
58+
margin: 0 auto;
59+
padding: 2rem 1.5rem;
60+
}
61+
62+
h1 {
63+
font-weight: 600;
64+
}
65+
66+
a {
67+
color: var(--accent);
68+
}
69+
70+
table {
71+
width: 100%;
72+
border-collapse: collapse;
73+
}
74+
75+
th,
76+
td {
77+
padding: 0.5rem 0.75rem;
78+
text-align: left;
79+
border-bottom: 1px solid var(--border);
80+
}
81+
82+
th {
83+
font-weight: 500;
84+
color: var(--muted);
85+
}
86+
87+
.pass {
88+
color: var(--pass);
89+
}
90+
91+
.fail {
92+
color: var(--fail);
93+
}
94+
95+
.muted {
96+
color: var(--muted);
97+
}
98+
99+
.field {
100+
margin-bottom: 1rem;
101+
}
102+
103+
label {
104+
display: block;
105+
margin-bottom: 0.25rem;
106+
color: var(--muted);
107+
}
108+
109+
input[type='text'],
110+
input[type='file'] {
111+
width: 100%;
112+
padding: 0.5rem;
113+
background-color: var(--panel);
114+
color: var(--fg);
115+
border: 1px solid var(--border);
116+
border-radius: 4px;
117+
}
118+
119+
button {
120+
padding: 0.5rem 1.25rem;
121+
font-weight: 600;
122+
color: #0b1622;
123+
background-color: var(--accent);
124+
border: 0;
125+
border-radius: 4px;
126+
cursor: pointer;
127+
}
128+
129+
button:hover {
130+
opacity: 0.9;
131+
}
132+
133+
pre {
134+
margin-top: 1.5rem;
135+
padding: 1rem;
136+
overflow: auto;
137+
background-color: var(--panel);
138+
border: 1px solid var(--border);
139+
border-radius: 4px;
140+
}

html/css/ess.css

Lines changed: 0 additions & 9 deletions
This file was deleted.

0 commit comments

Comments
 (0)