Merge pull request #74 from KasperskyLab/dev
PR of the version v0.7-test
This commit is contained in:
commit
7c1b0f7ced
@ -25,9 +25,6 @@
|
||||
<li class="menu-item">
|
||||
<span @click="$router.push('/device/db')">Manage database</span>
|
||||
</li>
|
||||
<!-- <li class="menu-item">
|
||||
<span @click="$router.push('/device/user')">User configuration</a>
|
||||
</li> -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -42,6 +39,9 @@
|
||||
<li class="menu-item">
|
||||
<span @click="$router.push('/iocs/search')">Search IOCs</span>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<span @click="$router.push('/iocs/misp')">MISP Instances</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -95,4 +95,4 @@
|
||||
.fade-leave-active {
|
||||
opacity: 0
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -650,4 +650,128 @@ h4, h5 {
|
||||
|
||||
.upper {
|
||||
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
|
||||
}
|
@ -34,6 +34,12 @@ const routes = [
|
||||
component: () => import('../views/iocs-manage.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/iocs/misp',
|
||||
name: 'iocs-manage',
|
||||
component: () => import('../views/iocs-misp.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/iocs/search',
|
||||
name: 'iocs-search',
|
||||
|
@ -35,6 +35,10 @@
|
||||
<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)
|
||||
</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">
|
||||
<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.
|
||||
|
175
app/backend/src/views/iocs-misp.vue
Normal file
175
app/backend/src/views/iocs-misp.vue
Normal 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>
|
@ -13,27 +13,37 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Indicator</th>
|
||||
<th>Type</th>
|
||||
<th>Tag</th>
|
||||
<th>TLP</th>
|
||||
<th> </th>
|
||||
<th>Source</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="r in results" :key="r.tlp">
|
||||
<td>{{ r.value }}</td>
|
||||
<td class="capi">{{ r.type }}</td>
|
||||
<td class="upper">{{ r.tag }}</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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else-if="first_search==false">
|
||||
<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 v-if="loading">
|
||||
<div class="empty">
|
||||
<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>
|
||||
@ -49,7 +59,8 @@ export default {
|
||||
return {
|
||||
results: [],
|
||||
first_search: true,
|
||||
jwt:""
|
||||
jwt:"",
|
||||
loading:false
|
||||
}
|
||||
},
|
||||
props: { },
|
||||
@ -57,6 +68,7 @@ export default {
|
||||
search_iocs: function() {
|
||||
this.results = []
|
||||
this.first_search = false
|
||||
this.loading = true;
|
||||
this.iocs.match(/[^\r\n]+/g).forEach(ioc => {
|
||||
ioc = ioc.trim()
|
||||
if("alert " != ioc.slice(0,6)) {
|
||||
@ -72,6 +84,7 @@ export default {
|
||||
if(response.data.results.length>0){
|
||||
this.results = [].concat(this.results, response.data.results);
|
||||
}
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
});
|
||||
|
@ -128,8 +128,8 @@
|
||||
}
|
||||
|
||||
.report-wrapper {
|
||||
width:90%;
|
||||
margin:auto;
|
||||
padding-bottom:1px;
|
||||
}
|
||||
|
||||
.device-ctx {
|
||||
@ -212,10 +212,11 @@
|
||||
.report-wrapper {
|
||||
width:60%;
|
||||
margin:auto;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.device-ctx {
|
||||
padding:15px;
|
||||
padding:15px 0px 15px 0px;
|
||||
margin:auto;
|
||||
}
|
||||
|
||||
@ -303,6 +304,17 @@ footer {
|
||||
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 {
|
||||
text-transform: uppercase;
|
||||
color : #999;
|
||||
|
@ -50,8 +50,11 @@
|
||||
"low_msg": "Només teniu {nb} alertes baixes, <br /> Si us plau comproveu-les.",
|
||||
"save_report": "Desa l'informe",
|
||||
"report_of": "Informe de",
|
||||
"ip_address": "Adreça IP",
|
||||
"mac_address": "Adreça MAC",
|
||||
"ip_address": "Adreça IP:",
|
||||
"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",
|
||||
"moderat": "moderat",
|
||||
"low": "baix"
|
||||
|
@ -50,8 +50,11 @@
|
||||
"low_msg": "Sie haben nur {nb} Warnungen der Stufe \"Niedrig\":<br> Überprüfen Sie sie gerne.",
|
||||
"save_report": "Bericht speichern",
|
||||
"report_of": "Bericht zu",
|
||||
"ip_address": "IP-Adresse",
|
||||
"mac_address": "MAC-Adresse",
|
||||
"ip_address": "IP-Adresse:",
|
||||
"mac_address": "MAC-Adresse:",
|
||||
"pcap_sha1": "SHA1:",
|
||||
"capture_started": "Capture begann mit:",
|
||||
"capture_ended": "Capture endete an:",
|
||||
"high": "Hoch",
|
||||
"moderate": "Mittel",
|
||||
"low": "Niedrig"
|
||||
@ -86,4 +89,4 @@
|
||||
"update_finished": "Update abgeschlossen, Sie werden weitergeleitet ...",
|
||||
"update_it": "Aktualisieren Sie TinyCheck"
|
||||
}
|
||||
}
|
||||
}
|
@ -50,8 +50,11 @@
|
||||
"low_msg": "You have only {nb} low alerts,<br /> don't hesitate to check them.",
|
||||
"save_report": "Save the report",
|
||||
"report_of": "Report of",
|
||||
"ip_address": "IP address",
|
||||
"mac_address": "MAC address",
|
||||
"ip_address": "IP address:",
|
||||
"mac_address": "MAC address:",
|
||||
"pcap_sha1": "SHA1:",
|
||||
"capture_started": "Capture started on:",
|
||||
"capture_ended": "Capture ended on:",
|
||||
"high": "high",
|
||||
"moderate": "moderate",
|
||||
"low": "low"
|
||||
|
@ -50,8 +50,11 @@
|
||||
"low_msg": "Solo tiene {nb} alertas bajas, <br /> por favor revíselas",
|
||||
"save_report": "Guardar el informe",
|
||||
"report_of": "Informe de",
|
||||
"ip_address": "dirección IP",
|
||||
"mac_address": "dirección MAC",
|
||||
"ip_address": "Dirección IP:",
|
||||
"mac_address": "Dirección MAC:",
|
||||
"pcap_sha1": "SHA1:",
|
||||
"capture_started": "Captura comenzó a:",
|
||||
"capture_ended": "Captura terminó a:",
|
||||
"high": "alta",
|
||||
"moderate": "moderada",
|
||||
"low": "bajo"
|
||||
@ -86,4 +89,4 @@
|
||||
"update_finished": "Actualización finalizada, actualizando la interfaz...",
|
||||
"update_it": "Actualizar ahora"
|
||||
}
|
||||
}
|
||||
}
|
@ -52,6 +52,9 @@
|
||||
"report_of": "Rapport de",
|
||||
"ip_address": "Adresse IP :",
|
||||
"mac_address": "Adresse MAC :",
|
||||
"pcap_sha1": "SHA1 :",
|
||||
"capture_started": "Capture débutée le :",
|
||||
"capture_ended": "Capture finie le :",
|
||||
"high": "elevee",
|
||||
"moderate": "moyenne",
|
||||
"low": "basse"
|
||||
|
@ -50,8 +50,11 @@
|
||||
"low_msg": "Sono presenti solo {nb} avvisi con priorità bassa<br /> da controllare.",
|
||||
"save_report": "Salva il rapporto",
|
||||
"report_of": "Rapporto di",
|
||||
"ip_address": "Indirizzo IP",
|
||||
"mac_address": "Indirizzo MAC",
|
||||
"ip_address": "Indirizzo IP:",
|
||||
"mac_address": "Indirizzo MAC:",
|
||||
"pcap_sha1": "SHA1:",
|
||||
"capture_started": "Cattura è iniziata su:",
|
||||
"capture_ended": "Cattura terminata su:",
|
||||
"high": "elevata",
|
||||
"moderate": "moderata",
|
||||
"low": "bassa"
|
||||
@ -86,4 +89,4 @@
|
||||
"update_finished": "Aggiornamento completato, verrai reindirizzato ...",
|
||||
"update_it": "Aggiorna TinyCheck"
|
||||
}
|
||||
}
|
||||
}
|
@ -50,8 +50,11 @@
|
||||
"low_msg": "Você tem apenas {nb} alertas leves,<br /> não deixe de verificá-los.",
|
||||
"save_report": "Salvar o relatório",
|
||||
"report_of": "Relatório de",
|
||||
"ip_address": "Endereço IP",
|
||||
"mac_address": "Endereço MAC",
|
||||
"ip_address": "Endereço IP:",
|
||||
"mac_address": "Endereço MAC:",
|
||||
"pcap_sha1": "SHA1:",
|
||||
"capture_started": "Captura iniciada em:",
|
||||
"capture_ended": "Captura terminou em:",
|
||||
"high": "crítico",
|
||||
"moderate": "moderado",
|
||||
"low": "leve"
|
||||
@ -86,4 +89,4 @@
|
||||
"update_finished": "Atualização concluída, você será redirecionado ...",
|
||||
"update_it": "Atualizar TinyCheck"
|
||||
}
|
||||
}
|
||||
}
|
@ -50,8 +50,11 @@
|
||||
"low_msg": "У вас {nb} предупреждение низкого уровня<br />, проверьте их.",
|
||||
"save_report": "Сохранить отчет",
|
||||
"report_of": "Отчет",
|
||||
"ip_address": "IP-адрес",
|
||||
"mac_address": "MAC-адрес",
|
||||
"ip_address": "IP-адрес:",
|
||||
"mac_address": "MAC-адрес:",
|
||||
"pcap_sha1": " SHA1:",
|
||||
"capture_started": "Захват начался:",
|
||||
"capture_ended": "захват закончился:",
|
||||
"high": "высокий",
|
||||
"moderate": "средний",
|
||||
"low": "низкий"
|
||||
@ -86,4 +89,4 @@
|
||||
"update_finished": "Обновление завершено, вы будете перенаправлены...",
|
||||
"update_it": "Обновить TinyCheck"
|
||||
}
|
||||
}
|
||||
}
|
@ -54,7 +54,8 @@ export default {
|
||||
this.running = false;
|
||||
router.replace({ name: 'report',
|
||||
params: { alerts : response.data.alerts,
|
||||
device : response.data.device,
|
||||
device : response.data.device,
|
||||
pcap : response.data.pcap,
|
||||
capture_token : this.capture_token } });
|
||||
}
|
||||
})
|
||||
|
@ -24,6 +24,8 @@ export default {
|
||||
var internet = this.internet
|
||||
if (window.config.iface_out.charAt(0) == 'e'){
|
||||
router.push({ name: 'generate-ap' });
|
||||
} else if (!window.config.choose_net && this.internet){
|
||||
router.push({ name: 'generate-ap' });
|
||||
} else {
|
||||
router.push({ name: 'wifi-select',
|
||||
params: { saved_ssid: saved_ssid,
|
||||
|
@ -46,8 +46,14 @@
|
||||
</div>
|
||||
<div v-else-if="show_report" class="report-wrapper">
|
||||
<div class="device-ctx">
|
||||
<h3 style="margin: 0;">{{ $t("report.report_of") }} {{device.name}}</h3>
|
||||
{{ $t("report.ip_address") }} {{device.ip_address}}<br />{{ $t("report.mac_address") }} {{device.mac_address}}
|
||||
<h3 style="margin: 0; padding-left:10px;">{{ $t("report.report_of") }} {{device.name}}</h3>
|
||||
<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>
|
||||
<ul class="alerts">
|
||||
<li class="alert" v-for="alert in alerts.high" :key="alert.message">
|
||||
@ -105,6 +111,7 @@ export default {
|
||||
},
|
||||
props: {
|
||||
device: Object,
|
||||
pcap: Object,
|
||||
alerts: Array,
|
||||
capture_token: String
|
||||
},
|
||||
|
@ -2,6 +2,7 @@ ipwhois
|
||||
M2Crypto
|
||||
pyOpenSSL
|
||||
pydig
|
||||
pymisp
|
||||
netaddr
|
||||
pyyaml
|
||||
flask
|
||||
@ -14,4 +15,4 @@ wifi
|
||||
qrcode
|
||||
netifaces
|
||||
weasyprint
|
||||
python-whois
|
||||
python-whois
|
||||
|
@ -17,3 +17,14 @@ CREATE TABLE "whitelist" (
|
||||
"added_on" INTEGER NOT NULL,
|
||||
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)
|
||||
);
|
||||
|
@ -51,6 +51,7 @@ frontend:
|
||||
virtual_keyboard: true
|
||||
user_lang: userlang
|
||||
update: updateoption
|
||||
choose_net: false
|
||||
|
||||
# NETWORK -
|
||||
# Some elements related to the network configuration, such as
|
||||
|
@ -441,7 +441,7 @@ change_configs() {
|
||||
|
||||
feeding_iocs() {
|
||||
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() {
|
||||
|
@ -26,6 +26,20 @@ def add(ioc_type, ioc_tag, ioc_tlp, ioc_value):
|
||||
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'])
|
||||
@require_header_token
|
||||
def delete(ioc_id):
|
||||
|
44
server/backend/app/blueprints/misp.py
Normal file
44
server/backend/app/blueprints/misp.py
Normal 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]})
|
@ -56,7 +56,12 @@ class IOCs(object):
|
||||
db.session.commit()
|
||||
return {"status": True,
|
||||
"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:
|
||||
return {"status": False,
|
||||
"message": "Wrong IOC format",
|
||||
@ -111,7 +116,8 @@ class IOCs(object):
|
||||
"type": ioc["type"],
|
||||
"tag": ioc["tag"],
|
||||
"tlp": ioc["tlp"],
|
||||
"value": ioc["value"]}
|
||||
"value": ioc["value"],
|
||||
"source": ioc["source"]}
|
||||
|
||||
@staticmethod
|
||||
def get_types():
|
||||
|
159
server/backend/app/classes/misp.py
Normal file
159
server/backend/app/classes/misp.py
Normal 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
|
@ -1,5 +1,6 @@
|
||||
from app import db
|
||||
|
||||
|
||||
class Ioc(db.Model):
|
||||
def __init__(self, value, type, tlp, tag, source, added_on):
|
||||
self.value = value
|
||||
@ -9,6 +10,7 @@ class Ioc(db.Model):
|
||||
self.source = source
|
||||
self.added_on = added_on
|
||||
|
||||
|
||||
class Whitelist(db.Model):
|
||||
def __init__(self, element, type, source, added_on):
|
||||
self.element = element
|
||||
@ -16,5 +18,17 @@ class Whitelist(db.Model):
|
||||
self.source = source
|
||||
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(Ioc, db.Table('iocs', db.metadata, autoload=True))
|
||||
db.mapper(MISPInst, db.Table('misp', db.metadata, autoload=True))
|
||||
|
@ -6,6 +6,7 @@ from app.decorators import auth
|
||||
from app.blueprints.ioc import ioc_bp
|
||||
from app.blueprints.whitelist import whitelist_bp
|
||||
from app.blueprints.config import config_bp
|
||||
from app.blueprints.misp import misp_bp
|
||||
import datetime
|
||||
import secrets
|
||||
import jwt
|
||||
@ -56,6 +57,7 @@ def page_not_found(e):
|
||||
app.register_blueprint(ioc_bp, url_prefix='/api/ioc')
|
||||
app.register_blueprint(whitelist_bp, url_prefix='/api/whitelist')
|
||||
app.register_blueprint(config_bp, url_prefix='/api/config')
|
||||
app.register_blueprint(misp_bp, url_prefix='/api/misp')
|
||||
|
||||
if __name__ == '__main__':
|
||||
ssl_cert = "{}/{}".format(path[0], 'cert.pem')
|
||||
|
@ -4,6 +4,7 @@
|
||||
from app.utils import read_config
|
||||
from app.classes.iocs import IOCs
|
||||
from app.classes.whitelist import WhiteList
|
||||
from app.classes.misp import MISP
|
||||
|
||||
import requests
|
||||
import json
|
||||
@ -16,11 +17,6 @@ from multiprocessing import Process
|
||||
in the configuration file. This in order to get
|
||||
automatically new iocs / elements from remote
|
||||
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)
|
||||
@ -29,7 +25,7 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
def watch_iocs():
|
||||
"""
|
||||
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
|
||||
@ -120,8 +116,32 @@ def watch_whitelists():
|
||||
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)
|
||||
p2 = Process(target=watch_whitelists)
|
||||
p3 = Process(target=watch_misp)
|
||||
|
||||
p1.start()
|
||||
p2.start()
|
||||
p3.start()
|
||||
|
@ -72,5 +72,6 @@ def get_config():
|
||||
"shutdown_option": read_config(("frontend", "shutdown_option")),
|
||||
"reboot_option": read_config(("frontend", "reboot_option")),
|
||||
"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"))
|
||||
})
|
||||
|
@ -41,13 +41,18 @@ class Analysis(object):
|
||||
:return: dict containing the report or error message.
|
||||
"""
|
||||
|
||||
device, alerts = {}, {}
|
||||
device, alerts, pcap = {}, {}, {}
|
||||
|
||||
# Getting device configuration.
|
||||
if os.path.isfile("/tmp/{}/assets/device.json".format(self.token)):
|
||||
with open("/tmp/{}/assets/device.json".format(self.token), "r") as 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.
|
||||
if os.path.isfile("/tmp/{}/assets/alerts.json".format(self.token)):
|
||||
with open("/tmp/{}/assets/alerts.json".format(self.token), "r") as f:
|
||||
@ -55,6 +60,7 @@ class Analysis(object):
|
||||
|
||||
if device != {} and alerts != {}:
|
||||
return {"alerts": alerts,
|
||||
"device": device}
|
||||
"device": device,
|
||||
"pcap": pcap}
|
||||
else:
|
||||
return {"message": "No report yet"}
|
||||
|
@ -1,337 +1,355 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import subprocess as sp
|
||||
import netifaces as ni
|
||||
import requests as rq
|
||||
import sys
|
||||
import time
|
||||
import qrcode
|
||||
import base64
|
||||
import random
|
||||
import requests
|
||||
|
||||
from wifi import Cell
|
||||
from os import path, remove
|
||||
from io import BytesIO
|
||||
from app.utils import terminate_process, read_config
|
||||
|
||||
|
||||
class Network(object):
|
||||
|
||||
def __init__(self):
|
||||
self.AP_SSID = False
|
||||
self.AP_PASS = False
|
||||
self.iface_in = read_config(("network", "in"))
|
||||
self.iface_out = read_config(("network", "out"))
|
||||
self.enable_interface(self.iface_in)
|
||||
self.enable_interface(self.iface_out)
|
||||
self.enable_forwarding()
|
||||
self.reset_dnsmasq_leases()
|
||||
self.random_choice_alphabet = "abcdef1234567890"
|
||||
|
||||
def check_status(self):
|
||||
"""
|
||||
The method check_status check the IP addressing of each interface
|
||||
and return their associated IP.
|
||||
|
||||
:return: dict containing each interface status.
|
||||
"""
|
||||
|
||||
ctx = {"interfaces": {
|
||||
self.iface_in: False,
|
||||
self.iface_out: False,
|
||||
"eth0": False},
|
||||
"internet": self.check_internet()}
|
||||
|
||||
for iface in ctx["interfaces"].keys():
|
||||
try:
|
||||
ip = ni.ifaddresses(iface)[ni.AF_INET][0]["addr"]
|
||||
if not ip.startswith("127") or not ip.startswith("169.254"):
|
||||
ctx["interfaces"][iface] = ip
|
||||
except:
|
||||
ctx["interfaces"][iface] = "Interface not connected or present."
|
||||
return ctx
|
||||
|
||||
def wifi_list_networks(self):
|
||||
"""
|
||||
The method wifi_list_networks list the available WiFi networks
|
||||
by using wifi python package.
|
||||
:return: dict - containing the list of Wi-Fi networks.
|
||||
"""
|
||||
networks = []
|
||||
try:
|
||||
for n in Cell.all(self.iface_out):
|
||||
if n.ssid not in [n["ssid"] for n in networks] and n.ssid and n.encrypted:
|
||||
networks.append(
|
||||
{"ssid": n.ssid, "type": n.encryption_type})
|
||||
return {"networks": networks}
|
||||
except:
|
||||
return {"networks": []}
|
||||
|
||||
@staticmethod
|
||||
def wifi_setup(ssid, password):
|
||||
"""
|
||||
Edit the wpa_supplicant file with provided credentials.
|
||||
If the ssid already exists, just update the password. Otherwise
|
||||
create a new entry in the file.
|
||||
|
||||
:return: dict containing the status of the operation
|
||||
"""
|
||||
if len(password) >= 8 and len(ssid):
|
||||
found = False
|
||||
networks = []
|
||||
header, content = "", ""
|
||||
|
||||
with open("/etc/wpa_supplicant/wpa_supplicant.conf") as f:
|
||||
content = f.read()
|
||||
blocks = content.split("network={")
|
||||
header = blocks[0]
|
||||
|
||||
for block in blocks[1:]:
|
||||
net = {}
|
||||
for line in block.splitlines():
|
||||
if line and line != "}":
|
||||
if "priority=10" not in line.strip():
|
||||
key, val = line.strip().split("=")
|
||||
if key != "disabled":
|
||||
net[key] = val.replace("\"", "")
|
||||
networks.append(net)
|
||||
|
||||
for net in networks:
|
||||
if net["ssid"] == ssid:
|
||||
net["psk"] = password.replace('"', '\\"')
|
||||
net["priority"] = "10"
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
networks.append({
|
||||
"ssid": ssid,
|
||||
"psk": password.replace('"', '\\"'),
|
||||
"key_mgmt": "WPA-PSK",
|
||||
"priority": "10"
|
||||
})
|
||||
|
||||
with open("/etc/wpa_supplicant/wpa_supplicant.conf", "w+") as f:
|
||||
content = header
|
||||
for network in networks:
|
||||
net = "network={\n"
|
||||
for k, v in network.items():
|
||||
if k in ["ssid", "psk"]:
|
||||
net += " {}=\"{}\"\n".format(k, v)
|
||||
else:
|
||||
net += " {}={}\n".format(k, v)
|
||||
net += "}\n\n"
|
||||
content += net
|
||||
if f.write(content):
|
||||
return {"status": True,
|
||||
"message": "Configuration saved"}
|
||||
else:
|
||||
return {"status": False,
|
||||
"message": "Error while writing wpa_supplicant configuration file."}
|
||||
else:
|
||||
return {"status": False,
|
||||
"message": "Empty SSID or/and password length less than 8 chars."}
|
||||
|
||||
def wifi_connect(self):
|
||||
"""
|
||||
Connect to one of the WiFi networks present in the wpa_supplicant.conf.
|
||||
:return: dict containing the TinyCheck <-> AP status.
|
||||
"""
|
||||
|
||||
# Kill wpa_supplicant instances, if any.
|
||||
terminate_process("wpa_supplicant")
|
||||
# Launch a new instance of wpa_supplicant.
|
||||
sp.Popen(["wpa_supplicant", "-B", "-i", self.iface_out, "-c",
|
||||
"/etc/wpa_supplicant/wpa_supplicant.conf"]).wait()
|
||||
# Check internet status
|
||||
for _ in range(1, 40):
|
||||
if self.check_internet():
|
||||
return {"status": True,
|
||||
"message": "Wifi connected"}
|
||||
time.sleep(1)
|
||||
|
||||
return {"status": False,
|
||||
"message": "Wifi not connected"}
|
||||
|
||||
def start_ap(self):
|
||||
"""
|
||||
The start_ap method generates an Access Point by using HostApd
|
||||
and provide to the GUI the associated ssid, password and qrcode.
|
||||
|
||||
:return: dict containing the status of the AP
|
||||
"""
|
||||
|
||||
# Re-ask to enable interface, sometimes it just go away.
|
||||
if not self.enable_interface(self.iface_out):
|
||||
return {"status": False,
|
||||
"message": "Interface not present."}
|
||||
|
||||
# Generate the hostapd configuration
|
||||
if read_config(("network", "tokenized_ssids")):
|
||||
token = "".join([random.choice(self.random_choice_alphabet)
|
||||
for i in range(4)])
|
||||
self.AP_SSID = random.choice(read_config(
|
||||
("network", "ssids"))) + "-" + token
|
||||
else:
|
||||
self.AP_SSID = random.choice(read_config(("network", "ssids")))
|
||||
self.AP_PASS = "".join(
|
||||
[random.choice(self.random_choice_alphabet) for i in range(8)])
|
||||
|
||||
# Launch hostapd
|
||||
if self.write_hostapd_config():
|
||||
if self.lauch_hostapd() and self.reset_dnsmasq_leases():
|
||||
return {"status": True,
|
||||
"message": "AP started",
|
||||
"ssid": self.AP_SSID,
|
||||
"password": self.AP_PASS,
|
||||
"qrcode": self.generate_qr_code()}
|
||||
else:
|
||||
return {"status": False,
|
||||
"message": "Error while creating AP."}
|
||||
else:
|
||||
return {"status": False,
|
||||
"message": "Error while writing hostapd configuration file."}
|
||||
|
||||
def generate_qr_code(self):
|
||||
"""
|
||||
The method generate_qr_code returns a QRCode based on
|
||||
the SSID and the password.
|
||||
|
||||
:return: - string containing the PNG of the QRCode.
|
||||
"""
|
||||
qrc = qrcode.make("WIFI:S:{};T:WPA;P:{};;".format(
|
||||
self.AP_SSID, self.AP_PASS))
|
||||
buffered = BytesIO()
|
||||
qrc.save(buffered, format="PNG")
|
||||
return "data:image/png;base64,{}".format(base64.b64encode(buffered.getvalue()).decode("utf8"))
|
||||
|
||||
def write_hostapd_config(self):
|
||||
"""
|
||||
The method write_hostapd_config write the hostapd configuration
|
||||
under a temporary location defined in the config file.
|
||||
|
||||
:return: bool - if hostapd configuration file created
|
||||
"""
|
||||
try:
|
||||
with open("{}/app/assets/hostapd.conf".format(sys.path[0]), "r") as f:
|
||||
conf = f.read()
|
||||
conf = conf.replace("{IFACE}", self.iface_in)
|
||||
conf = conf.replace("{SSID}", self.AP_SSID)
|
||||
conf = conf.replace("{PASS}", self.AP_PASS)
|
||||
with open("/tmp/hostapd.conf", "w") as c:
|
||||
c.write(conf)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def lauch_hostapd(self):
|
||||
"""
|
||||
The method lauch_hostapd kill old instance of hostapd and launch a
|
||||
new one as a background process.
|
||||
|
||||
:return: bool - if hostapd sucessfully launched.
|
||||
"""
|
||||
|
||||
# Kill potential zombies of hostapd
|
||||
terminate_process("hostapd")
|
||||
|
||||
sp.Popen(["ifconfig", self.iface_in, "up"]).wait()
|
||||
sp.Popen(
|
||||
"/usr/sbin/hostapd /tmp/hostapd.conf > /tmp/hostapd.log", shell=True)
|
||||
|
||||
while True:
|
||||
if path.isfile("/tmp/hostapd.log"):
|
||||
with open("/tmp/hostapd.log", "r") as f:
|
||||
log = f.read()
|
||||
err = ["Could not configure driver mode",
|
||||
"driver initialization failed"]
|
||||
if not any(e in log for e in err):
|
||||
if "AP-ENABLED" in log:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
time.sleep(1)
|
||||
|
||||
def stop_hostapd(self):
|
||||
"""
|
||||
Stop hostapd instance.
|
||||
|
||||
:return: dict - a little message for debug.
|
||||
"""
|
||||
if terminate_process("hostapd"):
|
||||
return {"status": True,
|
||||
"message": "AP stopped"}
|
||||
else:
|
||||
return {"status": False,
|
||||
"message": "No AP running"}
|
||||
|
||||
def reset_dnsmasq_leases(self):
|
||||
"""
|
||||
This method reset the DNSMasq leases and logs to get the new
|
||||
connected device name & new DNS entries.
|
||||
|
||||
:return: bool if everything goes well
|
||||
"""
|
||||
try:
|
||||
sp.Popen("service dnsmasq stop", shell=True).wait()
|
||||
sp.Popen("cp /dev/null /var/lib/misc/dnsmasq.leases",
|
||||
shell=True).wait()
|
||||
sp.Popen("cp /dev/null /var/log/messages.log", shell=True).wait()
|
||||
sp.Popen("service dnsmasq start", shell=True).wait()
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def enable_forwarding(self):
|
||||
"""
|
||||
This enable forwarding to get internet working on the connected device.
|
||||
Method tiggered during the Network class intialization.
|
||||
|
||||
:return: bool if everything goes well
|
||||
"""
|
||||
try:
|
||||
sp.Popen("echo 1 > /proc/sys/net/ipv4/ip_forward",
|
||||
shell=True).wait()
|
||||
|
||||
# Enable forwarding.
|
||||
sp.Popen(["iptables", "-A", "POSTROUTING", "-t", "nat", "-o",
|
||||
self.iface_out, "-j", "MASQUERADE"]).wait()
|
||||
|
||||
# Prevent the device to reach the 80 and 443 of TinyCheck.
|
||||
sp.Popen(["iptables", "-A", "INPUT", "-i", self.iface_in, "-d",
|
||||
"192.168.100.1", "-p", "tcp", "--match", "multiport", "--dports", "80,443", "-j" "DROP"]).wait()
|
||||
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def enable_interface(self, iface):
|
||||
"""
|
||||
This enable interfaces, with a simple check.
|
||||
:return: bool if everything goes well
|
||||
"""
|
||||
sh = sp.Popen(["ifconfig", iface],
|
||||
stdout=sp.PIPE, stderr=sp.PIPE)
|
||||
sh = sh.communicate()
|
||||
if b"<UP," in sh[0]:
|
||||
return True # The interface is up.
|
||||
elif sh[1]:
|
||||
return False # The interface doesn't exists (most of the cases).
|
||||
else:
|
||||
sp.Popen(["ifconfig", iface, "up"]).wait()
|
||||
return True
|
||||
|
||||
def check_internet(self):
|
||||
"""
|
||||
Check the internet link just with a small http request
|
||||
to an URL present in the configuration
|
||||
|
||||
:return: bool - if the request succeed or not.
|
||||
"""
|
||||
try:
|
||||
url = read_config(("network", "internet_check"))
|
||||
requests.get(url, timeout=10)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import subprocess as sp
|
||||
import netifaces as ni
|
||||
import requests as rq
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import qrcode
|
||||
import base64
|
||||
import random
|
||||
import requests
|
||||
|
||||
from wifi import Cell
|
||||
from os import path, remove
|
||||
from io import BytesIO
|
||||
from app.utils import terminate_process, read_config
|
||||
|
||||
|
||||
class Network(object):
|
||||
|
||||
def __init__(self):
|
||||
self.AP_SSID = False
|
||||
self.AP_PASS = False
|
||||
self.iface_in = read_config(("network", "in"))
|
||||
self.iface_out = read_config(("network", "out"))
|
||||
self.enable_interface(self.iface_in)
|
||||
self.enable_interface(self.iface_out)
|
||||
self.enable_forwarding()
|
||||
self.reset_dnsmasq_leases()
|
||||
self.random_choice_alphabet = "abcdef1234567890"
|
||||
|
||||
def check_status(self):
|
||||
"""
|
||||
The method check_status check the IP addressing of each interface
|
||||
and return their associated IP.
|
||||
|
||||
:return: dict containing each interface status.
|
||||
"""
|
||||
|
||||
ctx = {"interfaces": {
|
||||
self.iface_in: False,
|
||||
self.iface_out: False,
|
||||
"eth0": False},
|
||||
"internet": self.check_internet()}
|
||||
|
||||
for iface in ctx["interfaces"].keys():
|
||||
try:
|
||||
ip = ni.ifaddresses(iface)[ni.AF_INET][0]["addr"]
|
||||
if not ip.startswith("127") or not ip.startswith("169.254"):
|
||||
ctx["interfaces"][iface] = ip
|
||||
except:
|
||||
ctx["interfaces"][iface] = "Interface not connected or present."
|
||||
return ctx
|
||||
|
||||
def wifi_list_networks(self):
|
||||
"""
|
||||
The method wifi_list_networks list the available WiFi networks
|
||||
by using wifi python package.
|
||||
:return: dict - containing the list of Wi-Fi networks.
|
||||
"""
|
||||
networks = []
|
||||
try:
|
||||
for n in Cell.all(self.iface_out):
|
||||
if n.ssid not in [n["ssid"] for n in networks] and n.ssid and n.encrypted:
|
||||
networks.append(
|
||||
{"ssid": n.ssid, "type": n.encryption_type})
|
||||
return {"networks": networks}
|
||||
except:
|
||||
return {"networks": []}
|
||||
|
||||
@staticmethod
|
||||
def wifi_setup(ssid, password):
|
||||
"""
|
||||
Edit the wpa_supplicant file with provided credentials.
|
||||
If the ssid already exists, just update the password. Otherwise
|
||||
create a new entry in the file.
|
||||
|
||||
:return: dict containing the status of the operation
|
||||
"""
|
||||
if len(password) >= 8 and len(ssid):
|
||||
found = False
|
||||
networks = []
|
||||
header, content = "", ""
|
||||
|
||||
with open("/etc/wpa_supplicant/wpa_supplicant.conf") as f:
|
||||
content = f.read()
|
||||
blocks = content.split("network={")
|
||||
header = blocks[0]
|
||||
|
||||
for block in blocks[1:]:
|
||||
net = {}
|
||||
for line in block.splitlines():
|
||||
if line and line != "}":
|
||||
if "priority=10" not in line.strip():
|
||||
key, val = line.strip().split("=")
|
||||
if key != "disabled":
|
||||
net[key] = val.replace("\"", "")
|
||||
networks.append(net)
|
||||
|
||||
for net in networks:
|
||||
if net["ssid"] == ssid:
|
||||
net["psk"] = password.replace('"', '\\"')
|
||||
net["priority"] = "10"
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
networks.append({
|
||||
"ssid": ssid,
|
||||
"psk": password.replace('"', '\\"'),
|
||||
"key_mgmt": "WPA-PSK",
|
||||
"priority": "10"
|
||||
})
|
||||
|
||||
with open("/etc/wpa_supplicant/wpa_supplicant.conf", "w+") as f:
|
||||
content = header
|
||||
for network in networks:
|
||||
net = "network={\n"
|
||||
for k, v in network.items():
|
||||
if k in ["ssid", "psk"]:
|
||||
net += " {}=\"{}\"\n".format(k, v)
|
||||
else:
|
||||
net += " {}={}\n".format(k, v)
|
||||
net += "}\n\n"
|
||||
content += net
|
||||
if f.write(content):
|
||||
return {"status": True,
|
||||
"message": "Configuration saved"}
|
||||
else:
|
||||
return {"status": False,
|
||||
"message": "Error while writing wpa_supplicant configuration file."}
|
||||
else:
|
||||
return {"status": False,
|
||||
"message": "Empty SSID or/and password length less than 8 chars."}
|
||||
|
||||
def wifi_connect(self):
|
||||
"""
|
||||
Connect to one of the WiFi networks present in the wpa_supplicant.conf.
|
||||
:return: dict containing the TinyCheck <-> AP status.
|
||||
"""
|
||||
|
||||
# Kill wpa_supplicant instances, if any.
|
||||
terminate_process("wpa_supplicant")
|
||||
# Launch a new instance of wpa_supplicant.
|
||||
sp.Popen(["wpa_supplicant", "-B", "-i", self.iface_out, "-c",
|
||||
"/etc/wpa_supplicant/wpa_supplicant.conf"]).wait()
|
||||
# Check internet status
|
||||
for _ in range(1, 40):
|
||||
if self.check_internet():
|
||||
return {"status": True,
|
||||
"message": "Wifi connected"}
|
||||
time.sleep(1)
|
||||
|
||||
return {"status": False,
|
||||
"message": "Wifi not connected"}
|
||||
|
||||
def start_ap(self):
|
||||
"""
|
||||
The start_ap method generates an Access Point by using HostApd
|
||||
and provide to the GUI the associated ssid, password and qrcode.
|
||||
|
||||
:return: dict containing the status of the AP
|
||||
"""
|
||||
|
||||
# Re-ask to enable interface, sometimes it just go away.
|
||||
if not self.enable_interface(self.iface_out):
|
||||
return {"status": False,
|
||||
"message": "Interface not present."}
|
||||
|
||||
# Generate the hostapd configuration
|
||||
if read_config(("network", "tokenized_ssids")):
|
||||
token = "".join([random.choice(self.random_choice_alphabet)
|
||||
for i in range(4)])
|
||||
self.AP_SSID = random.choice(read_config(
|
||||
("network", "ssids"))) + "-" + token
|
||||
else:
|
||||
self.AP_SSID = random.choice(read_config(("network", "ssids")))
|
||||
self.AP_PASS = "".join(
|
||||
[random.choice(self.random_choice_alphabet) for i in range(8)])
|
||||
|
||||
# Launch hostapd
|
||||
if self.write_hostapd_config():
|
||||
if self.lauch_hostapd() and self.reset_dnsmasq_leases():
|
||||
return {"status": True,
|
||||
"message": "AP started",
|
||||
"ssid": self.AP_SSID,
|
||||
"password": self.AP_PASS,
|
||||
"qrcode": self.generate_qr_code()}
|
||||
else:
|
||||
return {"status": False,
|
||||
"message": "Error while creating AP."}
|
||||
else:
|
||||
return {"status": False,
|
||||
"message": "Error while writing hostapd configuration file."}
|
||||
|
||||
def generate_qr_code(self):
|
||||
"""
|
||||
The method generate_qr_code returns a QRCode based on
|
||||
the SSID and the password.
|
||||
|
||||
:return: - string containing the PNG of the QRCode.
|
||||
"""
|
||||
qrc = qrcode.make("WIFI:S:{};T:WPA;P:{};;".format(
|
||||
self.AP_SSID, self.AP_PASS))
|
||||
buffered = BytesIO()
|
||||
qrc.save(buffered, format="PNG")
|
||||
return "data:image/png;base64,{}".format(base64.b64encode(buffered.getvalue()).decode("utf8"))
|
||||
|
||||
def write_hostapd_config(self):
|
||||
"""
|
||||
The method write_hostapd_config write the hostapd configuration
|
||||
under a temporary location defined in the config file.
|
||||
|
||||
:return: bool - if hostapd configuration file created
|
||||
"""
|
||||
try:
|
||||
chan = self.set_ap_channel()
|
||||
with open("{}/app/assets/hostapd.conf".format(sys.path[0]), "r") as f:
|
||||
conf = f.read()
|
||||
conf = conf.replace("{IFACE}", self.iface_in)
|
||||
conf = conf.replace("{SSID}", self.AP_SSID)
|
||||
conf = conf.replace("{PASS}", self.AP_PASS)
|
||||
conf = conf.replace("{CHAN}", chan)
|
||||
with open("/tmp/hostapd.conf", "w") as c:
|
||||
c.write(conf)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def lauch_hostapd(self):
|
||||
"""
|
||||
The method lauch_hostapd kill old instance of hostapd and launch a
|
||||
new one as a background process.
|
||||
|
||||
:return: bool - if hostapd sucessfully launched.
|
||||
"""
|
||||
|
||||
# Kill potential zombies of hostapd
|
||||
terminate_process("hostapd")
|
||||
|
||||
sp.Popen(["ifconfig", self.iface_in, "up"]).wait()
|
||||
sp.Popen(
|
||||
"/usr/sbin/hostapd /tmp/hostapd.conf > /tmp/hostapd.log", shell=True)
|
||||
|
||||
while True:
|
||||
if path.isfile("/tmp/hostapd.log"):
|
||||
with open("/tmp/hostapd.log", "r") as f:
|
||||
log = f.read()
|
||||
err = ["Could not configure driver mode",
|
||||
"driver initialization failed"]
|
||||
if not any(e in log for e in err):
|
||||
if "AP-ENABLED" in log:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
time.sleep(1)
|
||||
|
||||
def stop_hostapd(self):
|
||||
"""
|
||||
Stop hostapd instance.
|
||||
|
||||
:return: dict - a little message for debug.
|
||||
"""
|
||||
if terminate_process("hostapd"):
|
||||
return {"status": True,
|
||||
"message": "AP stopped"}
|
||||
else:
|
||||
return {"status": False,
|
||||
"message": "No AP running"}
|
||||
|
||||
def reset_dnsmasq_leases(self):
|
||||
"""
|
||||
This method reset the DNSMasq leases and logs to get the new
|
||||
connected device name & new DNS entries.
|
||||
|
||||
:return: bool if everything goes well
|
||||
"""
|
||||
try:
|
||||
sp.Popen("service dnsmasq stop", shell=True).wait()
|
||||
sp.Popen("cp /dev/null /var/lib/misc/dnsmasq.leases",
|
||||
shell=True).wait()
|
||||
sp.Popen("cp /dev/null /var/log/messages.log", shell=True).wait()
|
||||
sp.Popen("service dnsmasq start", shell=True).wait()
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def enable_forwarding(self):
|
||||
"""
|
||||
This enable forwarding to get internet working on the connected device.
|
||||
Method tiggered during the Network class intialization.
|
||||
|
||||
:return: bool if everything goes well
|
||||
"""
|
||||
try:
|
||||
sp.Popen("echo 1 > /proc/sys/net/ipv4/ip_forward",
|
||||
shell=True).wait()
|
||||
|
||||
# Enable forwarding.
|
||||
sp.Popen(["iptables", "-A", "POSTROUTING", "-t", "nat", "-o",
|
||||
self.iface_out, "-j", "MASQUERADE"]).wait()
|
||||
|
||||
# Prevent the device to reach the 80 and 443 of TinyCheck.
|
||||
sp.Popen(["iptables", "-A", "INPUT", "-i", self.iface_in, "-d",
|
||||
"192.168.100.1", "-p", "tcp", "--match", "multiport", "--dports", "80,443", "-j" "DROP"]).wait()
|
||||
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def enable_interface(self, iface):
|
||||
"""
|
||||
This enable interfaces, with a simple check.
|
||||
:return: bool if everything goes well
|
||||
"""
|
||||
sh = sp.Popen(["ifconfig", iface],
|
||||
stdout=sp.PIPE, stderr=sp.PIPE)
|
||||
sh = sh.communicate()
|
||||
if b"<UP," in sh[0]:
|
||||
return True # The interface is up.
|
||||
elif sh[1]:
|
||||
return False # The interface doesn't exists (most of the cases).
|
||||
else:
|
||||
sp.Popen(["ifconfig", iface, "up"]).wait()
|
||||
return True
|
||||
|
||||
def check_internet(self):
|
||||
"""
|
||||
Check the internet link just with a small http request
|
||||
to an URL present in the configuration
|
||||
|
||||
:return: bool - if the request succeed or not.
|
||||
"""
|
||||
try:
|
||||
url = read_config(("network", "internet_check"))
|
||||
requests.get(url, timeout=10)
|
||||
return True
|
||||
except:
|
||||
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"
|
||||
|
12
update.sh
12
update.sh
@ -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/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."
|
||||
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
|
||||
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
|
||||
if ! grep -q choose_net /usr/share/tinycheck/config.yaml; then
|
||||
sed -i 's/frontend:/frontend:\n choose_net: false/g' /usr/share/tinycheck/config.yaml
|
||||
fi
|
||||
|
||||
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
|
||||
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
|
||||
sed -i "s/free_issuers:/free_issuers:\n - CN=R3,O=Let's Encrypt,C=US/g" /usr/share/tinycheck/config.yaml
|
||||
fi
|
||||
|
Loading…
Reference in New Issue
Block a user