Merge pull request #74 from KasperskyLab/dev

PR of the version v0.7-test
This commit is contained in:
Félix Aimé 2021-06-15 15:37:12 +02:00 committed by GitHub
commit 7c1b0f7ced
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1060 additions and 387 deletions

View File

@ -25,9 +25,6 @@
<li class="menu-item"> <li class="menu-item">
<span @click="$router.push('/device/db')">Manage database</span> <span @click="$router.push('/device/db')">Manage database</span>
</li> </li>
<!-- <li class="menu-item">
<span @click="$router.push('/device/user')">User configuration</a>
</li> -->
</ul> </ul>
</div> </div>
</div> </div>
@ -42,6 +39,9 @@
<li class="menu-item"> <li class="menu-item">
<span @click="$router.push('/iocs/search')">Search IOCs</span> <span @click="$router.push('/iocs/search')">Search IOCs</span>
</li> </li>
<li class="menu-item">
<span @click="$router.push('/iocs/misp')">MISP Instances</span>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -651,3 +651,127 @@ h4, h5 {
.upper { .upper {
text-transform: uppercase; text-transform: uppercase;
} }
/*** MISP CSS ***/
.misp-form {
/* Using CSS Grid to lay out the elements in two-dimensions: */
display: grid;
/* specifying a 0.2em gutter/gap between adjacent elements: */
gap: 0.4em;
overflow:auto;
grid-template-columns: 10em 0.5em 1fr;
width: 100%;
border-radius:.1rem;
margin-bottom: .8rem;
}
.misp-label {
/* placing all <label> elements in the grid column 1 (the first): */
grid-column: 1;
text-align: left;
padding-top: .3em;
}
.misp-offline {
background-color: #e85600;
color: #FFF;
font-size: 11px;
border-radius: 3px;
padding:3px 6px 3px 6px;
cursor: help;
}
.misp-online {
background-color: #64c800;
color: #FFF;
font-size: 11px;
border-radius: 3px;
padding:3px 6px 3px 6px;
cursor: help;
}
.misp-name {
font-size: 1rem;
font-family: "Roboto-Bold";
color: #484848;
}
.misp-name:disabled {
border-style: none;
color:inherit;
background-color: inherit;
padding: unset;
}
.misp-input {
grid-column: 3;
align-self: center;
}
.misp-input:disabled {
border-style: none;
color:inherit;
background-color: inherit;
padding: unset;
}
.misp-button {
/* positioning the <button> element in the grid-area identified
by the name of 'submit': */
grid-area: submit;
}
.loading {
margin-bottom: 12px;
display: block;
}
.loading::after {
animation: loading .5s infinite linear;
background: 0 0;
border: .1rem solid #66758c;
border-radius: 50%;
border-right-color: transparent;
border-top-color: transparent;
content: "";
display: block;
height: .8rem;
left: 50%;
margin-left: -.4rem;
margin-top: -.4rem;
opacity: 1;
padding: 0;
position: absolute;
top: 50%;
width: .8rem;
z-index: 1;
}
.loading.loading-lg::after {
height: 1.6rem;
margin-left: -.8rem;
margin-top: -.8rem;
width: 1.6rem;
}
.tooltip::after{
background:rgba(48,55,66,.95);
border-radius:3px;
bottom:100%;
color:#fff;
content:attr(data-tooltip);
display:block;
font-size: 11px;
left:50%;
max-width:320px;
opacity:0;
overflow:hidden;
padding:3px 6px 3px 6px;
pointer-events:none;
position:absolute;
text-overflow:ellipsis;
transform:translate(-50%,.4rem);
transition:opacity .2s,transform .2s;
white-space:pre;
z-index:300
}

View File

@ -34,6 +34,12 @@ const routes = [
component: () => import('../views/iocs-manage.vue'), component: () => import('../views/iocs-manage.vue'),
props: true props: true
}, },
{
path: '/iocs/misp',
name: 'iocs-manage',
component: () => import('../views/iocs-misp.vue'),
props: true
},
{ {
path: '/iocs/search', path: '/iocs/search',
name: 'iocs-search', name: 'iocs-search',

View File

@ -35,6 +35,10 @@
<input type="checkbox" @change="switch_config('frontend', 'virtual_keyboard')" v-model="config.frontend.virtual_keyboard"> <input type="checkbox" @change="switch_config('frontend', 'virtual_keyboard')" v-model="config.frontend.virtual_keyboard">
<i class="form-icon"></i> Use virtual keyboard (for touch screen) <i class="form-icon"></i> Use virtual keyboard (for touch screen)
</label> </label>
<label class="form-switch">
<input type="checkbox" @change="switch_config('frontend', 'choose_net')" v-model="config.frontend.choose_net">
<i class="form-icon"></i> Allow the end-user to choose the network even if connected.
</label>
<label class="form-switch"> <label class="form-switch">
<input type="checkbox" @change="switch_config('frontend', 'reboot_option')" v-model="config.frontend.reboot_option"> <input type="checkbox" @change="switch_config('frontend', 'reboot_option')" v-model="config.frontend.reboot_option">
<i class="form-icon"></i> Allow the end-user to reboot the device from the interface. <i class="form-icon"></i> Allow the end-user to reboot the device from the interface.

View File

@ -0,0 +1,175 @@
<template>
<div class="backend-content" id="content">
<div class="column col-8 col-xs-12">
<h3 class="s-title">Manage MISP instances</h3>
<ul class="tab tab-block">
<li class="tab-item">
<a href="#" v-on:click="switch_tab('addmisp')" v-bind:class="{ active: tabs.addmisp }">Add instance</a>
</li>
<li class="tab-item">
<a href="#" v-on:click="switch_tab('instances')" v-bind:class="{ active: tabs.instances }">Existing instances</a>
</li>
</ul>
<div v-if="tabs.addmisp">
<div class="misp-form">
<label class="misp-label">Instance name</label><span></span>
<input class="form-input" type="text" ref="misp_name" placeholder="CYBERACME MISP" v-model="mispinst.name" required>
<label class="misp-label">Instance URL</label><span></span>
<input class="form-input" type="text" ref="misp_url" placeholder="https://misp.cyberacme.com" v-model="mispinst.url" required>
<label class="misp-label">Authentication key</label><span></span>
<input class="form-input" type="text" ref="misp_key" placeholder="OqHSMyAuth3ntic4t10nK3y0MyAuth3ntic4t10nK3y3iiH" v-model="mispinst.key" required>
<label class="misp-label" v-if="mispinst.url.startsWith('https://')">Verify certificate? </label><span v-if="mispinst.url.startsWith('https://')"></span>
<div style="flex:50%" v-if="mispinst.url.startsWith('https://')"><label class="form-switch">
<input type="checkbox" v-model="mispinst.ssl">
<i class="form-icon"></i>
</label></div>
</div>
<button class="btn-primary btn col-12" v-on:click="add_instance()">Add MISP instance</button>
<div class="form-group" v-if="added">
<div class="toast toast-success">
MISP instance added successfully. Redirecting to instances in 2 seconds.
</div>
</div>
<div class="form-group" v-if="error">
<div class="toast toast-error">
MISP instance not added. {{error}}
</div>
</div>
</div>
<div class="form-group" v-if="tabs.instances">
<div v-if="instances.length">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Name</th>
<th>Server</th>
<th>Authkey</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr v-for="i in instances" v-bind:key="i.id">
<td>{{ i.name }}</td>
<td>{{ i.url.replace('https://', '') .replace('http://', '') }}</td>
<td>{{ i.apikey.slice(0,5) }} [...] {{ i.apikey.slice(35,40) }}</td>
<td>
<span v-if="i.connected" class="misp-online tooltip" :data-tooltip="i.lastsync"> ONLINE</span>
<span v-else class="misp-offline tooltip" :data-tooltip="i.lastsync"> OFFLINE</span>
</td>
<td><button class="btn btn-sm" v-on:click="delete_instance(i)">Delete</button></td>
</tr>
</tbody>
</table>
</div>
<div v-else>
<div class="empty">
<div v-if="loading">
<p class="empty-title h5">
<span class="loading loading-lg"></span>
</p>
<p class="empty-subtitle">Testing and loading your MISP instances.</p>
</div>
<div v-else>
<p class="empty-title h5">No MISP instance found.</p>
<p class="empty-subtitle">Do not hesitate to add a MISP instance.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'managemisp',
data() {
return {
error:false,
loading:false,
added:false,
mispinst:{ name:'', url:'',key:'', ssl:false },
instances:[],
tabs: { "addmisp" : true, "instances" : false },
jwt:""
}
},
props: { },
methods: {
add_instance: function()
{
this.added = false;
this.error = false;
if (this.mispinst.name && this.mispinst.url && this.mispinst.key)
{
axios.post(`/api/misp/add`, { data: { instance: this.mispinst } }, { headers: {'X-Token': this.jwt} }).then(response => {
if(response.data.status){
this.added = true;
setTimeout(function (){
this.switch_tab('instances')
this.mispinst = { name:'', url:'',key:'', ssl:false }
this.added = false
}.bind(this), 2000);
} else {
this.error = response.data.message;
}
})
.catch(err => (console.log(err)))
}
},
delete_instance(elem)
{
axios.get(`/api/misp/delete/${elem.id}`, { timeout: 10000, headers: {'X-Token': this.jwt} })
.then(response => {
if(response.data.status){
this.instances = this.instances.filter(function(el) { return el != elem; });
}
})
.catch(err => (console.log(err)))
},
get_misp_instances()
{
this.loading = true;
this.instances = []
axios.get(`/api/misp/get_all`, { timeout: 10000, headers: {'X-Token': this.jwt} })
.then(response => {
if(response.data.results){
this.instances = response.data.results;
this.instances.forEach(e => {
var lastsync = parseInt((Date.now()/1000 - e.lastsync) / 86400)
e.lastsync = (!lastsync)? "Synchronized today" : `Synchronized ${lastsync} day(s) ago`
} )
}
this.loading = false
})
.catch(err => (console.log(err)))
},
switch_tab: function(tab) {
Object.keys(this.tabs).forEach(key => {
if( key == tab ){
this.tabs[key] = true
if (key == "instances") this.get_misp_instances();
} else {
this.tabs[key] = false
}
});
},
get_jwt(){
axios.get(`/api/get-token`, { timeout: 10000 })
.then(response => {
if(response.data.token){
this.jwt = response.data.token
}
})
.catch(err => (console.log(err)))
}
},
created: function() {
this.get_jwt();
}
}
</script>

View File

@ -13,27 +13,37 @@
<thead> <thead>
<tr> <tr>
<th>Indicator</th> <th>Indicator</th>
<th>Type</th>
<th>Tag</th> <th>Tag</th>
<th>TLP</th> <th>TLP</th>
<th> </th> <th>Source</th>
<th>Action</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="r in results" :key="r.tlp"> <tr v-for="r in results" :key="r.tlp">
<td>{{ r.value }}</td> <td>{{ r.value }}</td>
<td class="capi">{{ r.type }}</td>
<td class="upper">{{ r.tag }}</td> <td class="upper">{{ r.tag }}</td>
<td><label :class="['tlp-' + r.tlp]">{{ r.tlp }}</label></td> <td><label :class="['tlp-' + r.tlp]">{{ r.tlp }}</label></td>
<td class="capi">{{ r.source }}</td>
<td><button class="btn btn-sm" v-on:click="remove(r)">Delete</button></td> <td><button class="btn btn-sm" v-on:click="remove(r)">Delete</button></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div v-else-if="first_search==false"> <div v-else-if="first_search==false">
<div class="empty"> <div v-if="loading">
<p class="empty-title h5">IOC<span v-if="this.iocs.match(/[^\r\n]+/g).length>1">s</span> not found.</p> <div class="empty">
<p class="empty-subtitle">Try wildcard search to expend your search.</p> <p class="empty-title h5">
<span class="loading loading-lg"></span>
</p>
<p class="empty-subtitle">Finding your IOC(s)...</p>
</div>
</div>
<div v-else>
<div class="empty">
<p class="empty-title h5">IOC<span v-if="this.iocs.match(/[^\r\n]+/g).length>1">s</span> not found.</p>
<p class="empty-subtitle">Try wildcard search to expend your search.</p>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -49,7 +59,8 @@ export default {
return { return {
results: [], results: [],
first_search: true, first_search: true,
jwt:"" jwt:"",
loading:false
} }
}, },
props: { }, props: { },
@ -57,6 +68,7 @@ export default {
search_iocs: function() { search_iocs: function() {
this.results = [] this.results = []
this.first_search = false this.first_search = false
this.loading = true;
this.iocs.match(/[^\r\n]+/g).forEach(ioc => { this.iocs.match(/[^\r\n]+/g).forEach(ioc => {
ioc = ioc.trim() ioc = ioc.trim()
if("alert " != ioc.slice(0,6)) { if("alert " != ioc.slice(0,6)) {
@ -72,6 +84,7 @@ export default {
if(response.data.results.length>0){ if(response.data.results.length>0){
this.results = [].concat(this.results, response.data.results); this.results = [].concat(this.results, response.data.results);
} }
this.loading = false;
}) })
.catch(err => (console.log(err))) .catch(err => (console.log(err)))
}); });

View File

@ -128,8 +128,8 @@
} }
.report-wrapper { .report-wrapper {
width:90%;
margin:auto; margin:auto;
padding-bottom:1px;
} }
.device-ctx { .device-ctx {
@ -212,10 +212,11 @@
.report-wrapper { .report-wrapper {
width:60%; width:60%;
margin:auto; margin:auto;
padding-bottom: 1px;
} }
.device-ctx { .device-ctx {
padding:15px; padding:15px 0px 15px 0px;
margin:auto; margin:auto;
} }
@ -303,6 +304,17 @@ footer {
padding-left:40px; padding-left:40px;
} }
.device-ctx-legend {
font-size: 14px;
color: #a6a6a6;
margin-top: 10px;
padding-top: 10px;
background-color: #fbfbfb;
padding: 10px;
border-radius: 5px;
}
.group-title { .group-title {
text-transform: uppercase; text-transform: uppercase;
color : #999; color : #999;

View File

@ -50,8 +50,11 @@
"low_msg": "Només teniu {nb} alertes baixes, <br /> Si us plau comproveu-les.", "low_msg": "Només teniu {nb} alertes baixes, <br /> Si us plau comproveu-les.",
"save_report": "Desa l'informe", "save_report": "Desa l'informe",
"report_of": "Informe de", "report_of": "Informe de",
"ip_address": "Adreça IP", "ip_address": "Adreça IP:",
"mac_address": "Adreça MAC", "mac_address": "Adreça MAC:",
"pcap_sha1": "SHA1:",
"capture_started": "La captura va començar a:",
"capture_ended": "La captura va acabar el:",
"high": "alt", "high": "alt",
"moderat": "moderat", "moderat": "moderat",
"low": "baix" "low": "baix"

View File

@ -50,8 +50,11 @@
"low_msg": "Sie haben nur {nb} Warnungen der Stufe \"Niedrig\":<br> Überprüfen Sie sie gerne.", "low_msg": "Sie haben nur {nb} Warnungen der Stufe \"Niedrig\":<br> Überprüfen Sie sie gerne.",
"save_report": "Bericht speichern", "save_report": "Bericht speichern",
"report_of": "Bericht zu", "report_of": "Bericht zu",
"ip_address": "IP-Adresse", "ip_address": "IP-Adresse:",
"mac_address": "MAC-Adresse", "mac_address": "MAC-Adresse:",
"pcap_sha1": "SHA1:",
"capture_started": "Capture begann mit:",
"capture_ended": "Capture endete an:",
"high": "Hoch", "high": "Hoch",
"moderate": "Mittel", "moderate": "Mittel",
"low": "Niedrig" "low": "Niedrig"

View File

@ -50,8 +50,11 @@
"low_msg": "You have only {nb} low alerts,<br /> don't hesitate to check them.", "low_msg": "You have only {nb} low alerts,<br /> don't hesitate to check them.",
"save_report": "Save the report", "save_report": "Save the report",
"report_of": "Report of", "report_of": "Report of",
"ip_address": "IP address", "ip_address": "IP address:",
"mac_address": "MAC address", "mac_address": "MAC address:",
"pcap_sha1": "SHA1:",
"capture_started": "Capture started on:",
"capture_ended": "Capture ended on:",
"high": "high", "high": "high",
"moderate": "moderate", "moderate": "moderate",
"low": "low" "low": "low"

View File

@ -50,8 +50,11 @@
"low_msg": "Solo tiene {nb} alertas bajas, <br /> por favor revíselas", "low_msg": "Solo tiene {nb} alertas bajas, <br /> por favor revíselas",
"save_report": "Guardar el informe", "save_report": "Guardar el informe",
"report_of": "Informe de", "report_of": "Informe de",
"ip_address": "dirección IP", "ip_address": "Dirección IP:",
"mac_address": "dirección MAC", "mac_address": "Dirección MAC:",
"pcap_sha1": "SHA1:",
"capture_started": "Captura comenzó a:",
"capture_ended": "Captura terminó a:",
"high": "alta", "high": "alta",
"moderate": "moderada", "moderate": "moderada",
"low": "bajo" "low": "bajo"

View File

@ -52,6 +52,9 @@
"report_of": "Rapport de", "report_of": "Rapport de",
"ip_address": "Adresse IP :", "ip_address": "Adresse IP :",
"mac_address": "Adresse MAC :", "mac_address": "Adresse MAC :",
"pcap_sha1": "SHA1 :",
"capture_started": "Capture débutée le :",
"capture_ended": "Capture finie le :",
"high": "elevee", "high": "elevee",
"moderate": "moyenne", "moderate": "moyenne",
"low": "basse" "low": "basse"

View File

@ -50,8 +50,11 @@
"low_msg": "Sono presenti solo {nb} avvisi con priorità bassa<br /> da controllare.", "low_msg": "Sono presenti solo {nb} avvisi con priorità bassa<br /> da controllare.",
"save_report": "Salva il rapporto", "save_report": "Salva il rapporto",
"report_of": "Rapporto di", "report_of": "Rapporto di",
"ip_address": "Indirizzo IP", "ip_address": "Indirizzo IP:",
"mac_address": "Indirizzo MAC", "mac_address": "Indirizzo MAC:",
"pcap_sha1": "SHA1:",
"capture_started": "Cattura è iniziata su:",
"capture_ended": "Cattura terminata su:",
"high": "elevata", "high": "elevata",
"moderate": "moderata", "moderate": "moderata",
"low": "bassa" "low": "bassa"

View File

@ -50,8 +50,11 @@
"low_msg": "Você tem apenas {nb} alertas leves,<br /> não deixe de verificá-los.", "low_msg": "Você tem apenas {nb} alertas leves,<br /> não deixe de verificá-los.",
"save_report": "Salvar o relatório", "save_report": "Salvar o relatório",
"report_of": "Relatório de", "report_of": "Relatório de",
"ip_address": "Endereço IP", "ip_address": "Endereço IP:",
"mac_address": "Endereço MAC", "mac_address": "Endereço MAC:",
"pcap_sha1": "SHA1:",
"capture_started": "Captura iniciada em:",
"capture_ended": "Captura terminou em:",
"high": "crítico", "high": "crítico",
"moderate": "moderado", "moderate": "moderado",
"low": "leve" "low": "leve"

View File

@ -50,8 +50,11 @@
"low_msg": "У вас {nb} предупреждение низкого уровня<br />, проверьте их.", "low_msg": "У вас {nb} предупреждение низкого уровня<br />, проверьте их.",
"save_report": "Сохранить отчет", "save_report": "Сохранить отчет",
"report_of": "Отчет", "report_of": "Отчет",
"ip_address": "IP-адрес", "ip_address": "IP-адрес:",
"mac_address": "MAC-адрес", "mac_address": "MAC-адрес:",
"pcap_sha1": " SHA1:",
"capture_started": "Захват начался:",
"capture_ended": "захват закончился:",
"high": "высокий", "high": "высокий",
"moderate": "средний", "moderate": "средний",
"low": "низкий" "low": "низкий"

View File

@ -55,6 +55,7 @@ export default {
router.replace({ name: 'report', router.replace({ name: 'report',
params: { alerts : response.data.alerts, params: { alerts : response.data.alerts,
device : response.data.device, device : response.data.device,
pcap : response.data.pcap,
capture_token : this.capture_token } }); capture_token : this.capture_token } });
} }
}) })

View File

@ -24,6 +24,8 @@ export default {
var internet = this.internet var internet = this.internet
if (window.config.iface_out.charAt(0) == 'e'){ if (window.config.iface_out.charAt(0) == 'e'){
router.push({ name: 'generate-ap' }); router.push({ name: 'generate-ap' });
} else if (!window.config.choose_net && this.internet){
router.push({ name: 'generate-ap' });
} else { } else {
router.push({ name: 'wifi-select', router.push({ name: 'wifi-select',
params: { saved_ssid: saved_ssid, params: { saved_ssid: saved_ssid,

View File

@ -46,8 +46,14 @@
</div> </div>
<div v-else-if="show_report" class="report-wrapper"> <div v-else-if="show_report" class="report-wrapper">
<div class="device-ctx"> <div class="device-ctx">
<h3 style="margin: 0;">{{ $t("report.report_of") }} {{device.name}}</h3> <h3 style="margin: 0; padding-left:10px;">{{ $t("report.report_of") }} {{device.name}}</h3>
{{ $t("report.ip_address") }} {{device.ip_address}}<br />{{ $t("report.mac_address") }} {{device.mac_address}} <div class="device-ctx-legend">
{{ $t("report.pcap_sha1") }} {{ pcap.SHA1 }}<br />
{{ $t("report.capture_started") }} {{ pcap["First packet time"].split(",")[0] }}<br />
{{ $t("report.capture_ended") }} {{ pcap["Last packet time"].split(",")[0] }}<br />
<!-- {{ $t("report.ip_address") }} {{device.ip_address}}<br /> -->
{{ $t("report.mac_address") }} {{device.mac_address}}
</div>
</div> </div>
<ul class="alerts"> <ul class="alerts">
<li class="alert" v-for="alert in alerts.high" :key="alert.message"> <li class="alert" v-for="alert in alerts.high" :key="alert.message">
@ -105,6 +111,7 @@ export default {
}, },
props: { props: {
device: Object, device: Object,
pcap: Object,
alerts: Array, alerts: Array,
capture_token: String capture_token: String
}, },

View File

@ -2,6 +2,7 @@ ipwhois
M2Crypto M2Crypto
pyOpenSSL pyOpenSSL
pydig pydig
pymisp
netaddr netaddr
pyyaml pyyaml
flask flask

View File

@ -17,3 +17,14 @@ CREATE TABLE "whitelist" (
"added_on" INTEGER NOT NULL, "added_on" INTEGER NOT NULL,
PRIMARY KEY("id" AUTOINCREMENT) PRIMARY KEY("id" AUTOINCREMENT)
); );
CREATE TABLE "misp" (
"id" INTEGER UNIQUE,
"name" TEXT,
"url" TEXT NOT NULL,
"apikey" TEXT NOT NULL,
"verifycert" INTEGER NOT NULL DEFAULT 0,
"added_on" NUMERIC NOT NULL,
"last_sync" NUMERIC NOT NULL DEFAULT 0,
PRIMARY KEY("id" AUTOINCREMENT)
);

View File

@ -51,6 +51,7 @@ frontend:
virtual_keyboard: true virtual_keyboard: true
user_lang: userlang user_lang: userlang
update: updateoption update: updateoption
choose_net: false
# NETWORK - # NETWORK -
# Some elements related to the network configuration, such as # Some elements related to the network configuration, such as

View File

@ -441,7 +441,7 @@ change_configs() {
feeding_iocs() { feeding_iocs() {
echo -e "\e[39m[+] Feeding your TinyCheck instance with fresh IOCs and whitelist, please wait." echo -e "\e[39m[+] Feeding your TinyCheck instance with fresh IOCs and whitelist, please wait."
python3 /usr/share/tinycheck/server/backend/watchers.py python3 /usr/share/tinycheck/server/backend/watchers.py 2>/dev/null
} }
reboot_box() { reboot_box() {

View File

@ -26,6 +26,20 @@ def add(ioc_type, ioc_tag, ioc_tlp, ioc_value):
return jsonify(res) return jsonify(res)
@ioc_bp.route('/add_post', methods=['POST'])
@require_header_token
def add_post():
"""
Parse and add an IOC to the database using the post method.
:return: status of the operation in JSON
"""
data = json.loads(request.data)
ioc = data["data"]["ioc"]
res = IOCs.add(ioc["ioc_type"], ioc["ioc_tag"], ioc["ioc_tlp"], ioc["ioc_value"], ioc["ioc_source"])
return jsonify(res)
@ioc_bp.route('/delete/<ioc_id>', methods=['GET']) @ioc_bp.route('/delete/<ioc_id>', methods=['GET'])
@require_header_token @require_header_token
def delete(ioc_id): def delete(ioc_id):

View File

@ -0,0 +1,44 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Blueprint, jsonify, Response, request
from app.decorators import require_header_token, require_get_token
from app.classes.misp import MISP
import json
misp_bp = Blueprint("misp", __name__)
misp = MISP()
@misp_bp.route('/add', methods=['POST'])
@require_header_token
def add_instance():
"""
Parse and add a MISP instance to the database.
:return: status of the operation in JSON
"""
data = json.loads(request.data)
res = misp.add_instance(data["data"]["instance"])
return jsonify(res)
@misp_bp.route('/delete/<misp_id>', methods=['GET'])
@require_header_token
def delete_instance(misp_id):
"""
Delete a MISP instance by its id to the database.
:return: status of the operation in JSON
"""
res = misp.delete_instance(misp_id)
return jsonify(res)
@misp_bp.route('/get_all', methods=['GET'])
@require_header_token
def get_all():
"""
Retreive a list of all MISP instances.
:return: list of MISP instances in JSON.
"""
res = misp.get_instances()
return jsonify({"results": [i for i in res]})

View File

@ -56,7 +56,12 @@ class IOCs(object):
db.session.commit() db.session.commit()
return {"status": True, return {"status": True,
"message": "IOC added", "message": "IOC added",
"ioc": escape(ioc_value)} "ioc": escape(ioc_value),
"type": escape(ioc_type),
"tlp": escape(ioc_tlp),
"tag": escape(ioc_tag),
"source": escape(source),
"added_on": escape(added_on)}
else: else:
return {"status": False, return {"status": False,
"message": "Wrong IOC format", "message": "Wrong IOC format",
@ -111,7 +116,8 @@ class IOCs(object):
"type": ioc["type"], "type": ioc["type"],
"tag": ioc["tag"], "tag": ioc["tag"],
"tlp": ioc["tlp"], "tlp": ioc["tlp"],
"value": ioc["value"]} "value": ioc["value"],
"source": ioc["source"]}
@staticmethod @staticmethod
def get_types(): def get_types():

View File

@ -0,0 +1,159 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from app import db
from app.db.models import MISPInst
from app.definitions import definitions as defs
from sqlalchemy.sql import exists
from urllib.parse import unquote
from flask import escape
from pymisp import PyMISP
import re
import time
import sys
class MISP(object):
def __init__(self):
return None
def add_instance(self, instance):
"""
Parse and add a MISP instance to the database.
:return: status of the operation in JSON
"""
url = instance["url"]
name = instance["name"]
apikey = instance["key"]
verify = instance["ssl"]
last_sync = int(time.time()-31536000) # One year
sameinstances = db.session.query(MISPInst).filter(
MISPInst.url == url, MISPInst.apikey == apikey)
if sameinstances.count():
return {"status": False,
"message": "This MISP instance already exists"}
if name:
if self.test_instance(url, apikey, verify):
added_on = int(time.time())
db.session.add(MISPInst(name, escape(
url), apikey, verify, added_on, last_sync))
db.session.commit()
return {"status": True,
"message": "MISP instance added"}
else:
return {"status": False,
"message": "Please verify the connection to the MISP instance"}
else:
return {"status": False,
"message": "Please provide a name for your instance"}
@staticmethod
def delete_instance(misp_id):
"""
Delete a MISP instance by its id in the database.
:return: status of the operation in JSON
"""
if db.session.query(exists().where(MISPInst.id == misp_id)).scalar():
db.session.query(MISPInst).filter_by(id=misp_id).delete()
db.session.commit()
return {"status": True,
"message": "MISP instance deleted"}
else:
return {"status": False,
"message": "MISP instance not found"}
def get_instances(self):
"""
Get MISP instances from the database
:return: generator of the records.
"""
for misp in db.session.query(MISPInst).all():
misp = misp.__dict__
yield {"id": misp["id"],
"name": misp["name"],
"url": misp["url"],
"apikey": misp["apikey"],
"verifycert": True if misp["verifycert"] else False,
"connected": self.test_instance(misp["url"], misp["apikey"], misp["verifycert"]),
"lastsync": misp["last_sync"]}
@staticmethod
def test_instance(url, apikey, verify):
"""
Test the connection of the MISP instance.
:return: generator of the records.
"""
try:
PyMISP(url, apikey, verify)
return True
except:
return False
@staticmethod
def update_sync(misp_id):
"""
Update the last synchronization date by the actual date.
:return: bool, True if updated.
"""
try:
misp = MISPInst.query.get(int(misp_id))
misp.last_sync = int(time.time())
db.session.commit()
return True
except:
return False
@staticmethod
def get_iocs(misp_id):
"""
Get all IOCs from specific MISP instance
:return: generator containing the IOCs.
"""
misp = MISPInst.query.get(int(misp_id))
if misp is not None:
if misp.url and misp.apikey:
try:
# Connect to MISP instance and get network activity attributes.
m = PyMISP(misp.url, misp.apikey, misp.verifycert)
r = m.search("attributes", category="Network activity", date_from=int(misp.last_sync))
except:
print("Unable to connect to the MISP instance ({}/{}).".format(misp.url, misp.apikey))
return []
for attr in r["Attribute"]:
if attr["type"] in ["ip-dst", "domain", "snort", "x509-fingerprint-sha1"]:
ioc = {"value": attr["value"],
"type": None,
"tag": "suspect",
"tlp": "white"}
# Deduce the IOC type.
if re.match(defs["iocs_types"][0]["regex"], attr["value"]):
ioc["type"] = "ip4addr"
elif re.match(defs["iocs_types"][1]["regex"], attr["value"]):
ioc["type"] = "ip6addr"
elif re.match(defs["iocs_types"][2]["regex"], attr["value"]):
ioc["type"] = "cidr"
elif re.match(defs["iocs_types"][3]["regex"], attr["value"]):
ioc["type"] = "domain"
elif re.match(defs["iocs_types"][4]["regex"], attr["value"]):
ioc["type"] = "sha1cert"
elif "alert " in attr["value"][0:6]:
ioc["type"] = "snort"
else:
continue
if "Tag" in attr:
for tag in attr["Tag"]:
# Add a TLP to the IOC if defined in tags.
tlp = re.search(r"^(?:tlp:)(red|green|amber|white)", tag['name'].lower())
if tlp: ioc["tlp"] = tlp.group(1)
# Add possible tag (need to match TinyCheck tags)
if tag["name"].lower() in [t["tag"] for t in defs["iocs_tags"]]:
ioc["tag"] = tag["name"].lower()
yield ioc

View File

@ -1,5 +1,6 @@
from app import db from app import db
class Ioc(db.Model): class Ioc(db.Model):
def __init__(self, value, type, tlp, tag, source, added_on): def __init__(self, value, type, tlp, tag, source, added_on):
self.value = value self.value = value
@ -9,6 +10,7 @@ class Ioc(db.Model):
self.source = source self.source = source
self.added_on = added_on self.added_on = added_on
class Whitelist(db.Model): class Whitelist(db.Model):
def __init__(self, element, type, source, added_on): def __init__(self, element, type, source, added_on):
self.element = element self.element = element
@ -16,5 +18,17 @@ class Whitelist(db.Model):
self.source = source self.source = source
self.added_on = added_on self.added_on = added_on
class MISPInst(db.Model):
def __init__(self, name, url, key, ssl, added_on, last_sync):
self.name = name
self.url = url
self.apikey = key
self.verifycert = ssl
self.added_on = added_on
self.last_sync = last_sync
db.mapper(Whitelist, db.Table('whitelist', db.metadata, autoload=True)) db.mapper(Whitelist, db.Table('whitelist', db.metadata, autoload=True))
db.mapper(Ioc, db.Table('iocs', db.metadata, autoload=True)) db.mapper(Ioc, db.Table('iocs', db.metadata, autoload=True))
db.mapper(MISPInst, db.Table('misp', db.metadata, autoload=True))

View File

@ -6,6 +6,7 @@ from app.decorators import auth
from app.blueprints.ioc import ioc_bp from app.blueprints.ioc import ioc_bp
from app.blueprints.whitelist import whitelist_bp from app.blueprints.whitelist import whitelist_bp
from app.blueprints.config import config_bp from app.blueprints.config import config_bp
from app.blueprints.misp import misp_bp
import datetime import datetime
import secrets import secrets
import jwt import jwt
@ -56,6 +57,7 @@ def page_not_found(e):
app.register_blueprint(ioc_bp, url_prefix='/api/ioc') app.register_blueprint(ioc_bp, url_prefix='/api/ioc')
app.register_blueprint(whitelist_bp, url_prefix='/api/whitelist') app.register_blueprint(whitelist_bp, url_prefix='/api/whitelist')
app.register_blueprint(config_bp, url_prefix='/api/config') app.register_blueprint(config_bp, url_prefix='/api/config')
app.register_blueprint(misp_bp, url_prefix='/api/misp')
if __name__ == '__main__': if __name__ == '__main__':
ssl_cert = "{}/{}".format(path[0], 'cert.pem') ssl_cert = "{}/{}".format(path[0], 'cert.pem')

View File

@ -4,6 +4,7 @@
from app.utils import read_config from app.utils import read_config
from app.classes.iocs import IOCs from app.classes.iocs import IOCs
from app.classes.whitelist import WhiteList from app.classes.whitelist import WhiteList
from app.classes.misp import MISP
import requests import requests
import json import json
@ -16,11 +17,6 @@ from multiprocessing import Process
in the configuration file. This in order to get in the configuration file. This in order to get
automatically new iocs / elements from remote automatically new iocs / elements from remote
sources without user interaction. sources without user interaction.
As of today the default export JSON format from
the backend and unauthenticated HTTP requests
are accepted. The code is little awkward, it'll
be better in a next version ;)
""" """
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
@ -29,7 +25,7 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def watch_iocs(): def watch_iocs():
""" """
Retrieve IOCs from the remote URLs defined in config/watchers. Retrieve IOCs from the remote URLs defined in config/watchers.
For each (new ?) IOC, add it to the DB. For each IOC, add it to the DB.
""" """
# Retrieve the URLs from the configuration # Retrieve the URLs from the configuration
@ -120,8 +116,32 @@ def watch_whitelists():
break break
def watch_misp():
"""
Retrieve IOCs from misp instances. Each new element is
tested and then added to the database.
"""
iocs, misp = IOCs(), MISP()
instances = [i for i in misp.get_instances()]
while instances:
for i, ist in enumerate(instances):
status = misp.test_instance(ist["url"],
ist["apikey"],
ist["verifycert"])
if status:
for ioc in misp.get_iocs(ist["id"]):
iocs.add(ioc["type"], ioc["tag"], ioc["tlp"],
ioc["value"], "misp-{}".format(ist["id"]))
misp.update_sync(ist["id"])
instances.pop(i)
if instances: time.sleep(60)
p1 = Process(target=watch_iocs) p1 = Process(target=watch_iocs)
p2 = Process(target=watch_whitelists) p2 = Process(target=watch_whitelists)
p3 = Process(target=watch_misp)
p1.start() p1.start()
p2.start() p2.start()
p3.start()

View File

@ -72,5 +72,6 @@ def get_config():
"shutdown_option": read_config(("frontend", "shutdown_option")), "shutdown_option": read_config(("frontend", "shutdown_option")),
"reboot_option": read_config(("frontend", "reboot_option")), "reboot_option": read_config(("frontend", "reboot_option")),
"iface_out": read_config(("network", "out")), "iface_out": read_config(("network", "out")),
"user_lang": read_config(("frontend", "user_lang")) "user_lang": read_config(("frontend", "user_lang")),
"choose_net": read_config(("frontend", "choose_net"))
}) })

View File

@ -41,13 +41,18 @@ class Analysis(object):
:return: dict containing the report or error message. :return: dict containing the report or error message.
""" """
device, alerts = {}, {} device, alerts, pcap = {}, {}, {}
# Getting device configuration. # Getting device configuration.
if os.path.isfile("/tmp/{}/assets/device.json".format(self.token)): if os.path.isfile("/tmp/{}/assets/device.json".format(self.token)):
with open("/tmp/{}/assets/device.json".format(self.token), "r") as f: with open("/tmp/{}/assets/device.json".format(self.token), "r") as f:
device = json.load(f) device = json.load(f)
# Getting pcap infos.
if os.path.isfile("/tmp/{}/assets/capinfos.json".format(self.token)):
with open("/tmp/{}/assets/capinfos.json".format(self.token), "r") as f:
pcap = json.load(f)
# Getting alerts configuration. # Getting alerts configuration.
if os.path.isfile("/tmp/{}/assets/alerts.json".format(self.token)): if os.path.isfile("/tmp/{}/assets/alerts.json".format(self.token)):
with open("/tmp/{}/assets/alerts.json".format(self.token), "r") as f: with open("/tmp/{}/assets/alerts.json".format(self.token), "r") as f:
@ -55,6 +60,7 @@ class Analysis(object):
if device != {} and alerts != {}: if device != {} and alerts != {}:
return {"alerts": alerts, return {"alerts": alerts,
"device": device} "device": device,
"pcap": pcap}
else: else:
return {"message": "No report yet"} return {"message": "No report yet"}

View File

@ -4,6 +4,7 @@
import subprocess as sp import subprocess as sp
import netifaces as ni import netifaces as ni
import requests as rq import requests as rq
import re
import sys import sys
import time import time
import qrcode import qrcode
@ -214,11 +215,13 @@ class Network(object):
:return: bool - if hostapd configuration file created :return: bool - if hostapd configuration file created
""" """
try: try:
chan = self.set_ap_channel()
with open("{}/app/assets/hostapd.conf".format(sys.path[0]), "r") as f: with open("{}/app/assets/hostapd.conf".format(sys.path[0]), "r") as f:
conf = f.read() conf = f.read()
conf = conf.replace("{IFACE}", self.iface_in) conf = conf.replace("{IFACE}", self.iface_in)
conf = conf.replace("{SSID}", self.AP_SSID) conf = conf.replace("{SSID}", self.AP_SSID)
conf = conf.replace("{PASS}", self.AP_PASS) conf = conf.replace("{PASS}", self.AP_PASS)
conf = conf.replace("{CHAN}", chan)
with open("/tmp/hostapd.conf", "w") as c: with open("/tmp/hostapd.conf", "w") as c:
c.write(conf) c.write(conf)
return True return True
@ -335,3 +338,18 @@ class Network(object):
return True return True
except: except:
return False return False
def set_ap_channel(self):
"""
Deduce the channel to have for the AP in order to prevent
kind of jamming between the two wifi interfaces.
"""
# Get the channel of the connected interface
sh = sp.Popen(["iw", self.iface_out, "info"],
stdout=sp.PIPE, stderr=sp.PIPE).communicate()
res = re.search("channel ([0-9]{1,2})", sh[0].decode('utf8'))
chn = res.group(1)
# Return a good candidate.
return "11" if int(chn) < 7 else "1"

View File

@ -40,13 +40,17 @@ elif [ $PWD = "/tmp/tinycheck" ]; then
cd /usr/share/tinycheck/app/frontend/ && npm install && npm audit fix && npm run build cd /usr/share/tinycheck/app/frontend/ && npm install && npm audit fix && npm run build
cd /usr/share/tinycheck/app/backend/ && npm install && npm audit fix && npm run build cd /usr/share/tinycheck/app/backend/ && npm install && npm audit fix && npm run build
echo "[+] Updating the database scheme..."
cd /usr/share/tinycheck/
sqlite3 tinycheck.sqlite3 < /tmp/tinycheck/assets/scheme.sql 2>/dev/null
echo "[+] Updating current configuration with new values." echo "[+] Updating current configuration with new values."
if ! grep -q reboot_option /usr/share/tinycheck/config.yaml; then if ! grep -q reboot_option /usr/share/tinycheck/config.yaml; then
sed -i 's/frontend:/frontend:\n reboot_option: true/g' /usr/share/tinycheck/config.yaml sed -i 's/frontend:/frontend:\n reboot_option: true/g' /usr/share/tinycheck/config.yaml
fi fi
if ! grep -q user_lang /usr/share/tinycheck/config.yaml; then if ! grep -q choose_net /usr/share/tinycheck/config.yaml; then
sed -i 's/frontend:/frontend:\n user_lang: en/g' /usr/share/tinycheck/config.yaml sed -i 's/frontend:/frontend:\n choose_net: false/g' /usr/share/tinycheck/config.yaml
fi fi
if ! grep -q shutdown_option /usr/share/tinycheck/config.yaml; then if ! grep -q shutdown_option /usr/share/tinycheck/config.yaml; then
@ -65,6 +69,10 @@ elif [ $PWD = "/tmp/tinycheck" ]; then
sed -i 's/frontend:/frontend:\n update: true/g' /usr/share/tinycheck/config.yaml sed -i 's/frontend:/frontend:\n update: true/g' /usr/share/tinycheck/config.yaml
fi fi
if ! grep -q user_lang /usr/share/tinycheck/config.yaml; then
sed -i 's/frontend:/frontend:\n user_lang: en/g' /usr/share/tinycheck/config.yaml
fi
if ! grep -q "CN=R3,O=Let's Encrypt,C=US" /usr/share/tinycheck/config.yaml; then if ! grep -q "CN=R3,O=Let's Encrypt,C=US" /usr/share/tinycheck/config.yaml; then
sed -i "s/free_issuers:/free_issuers:\n - CN=R3,O=Let's Encrypt,C=US/g" /usr/share/tinycheck/config.yaml sed -i "s/free_issuers:/free_issuers:\n - CN=R3,O=Let's Encrypt,C=US/g" /usr/share/tinycheck/config.yaml
fi fi