forked from Normation/rudder-plugins
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
443 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
import requests, json, glpi_logger | ||
|
||
# TODO clean initalizing to create the adequate ticket | ||
|
||
# Class used to create the php filter to look for objects in the glpi api | ||
class criterias: | ||
def __init__(self): | ||
self.array = [] | ||
|
||
def addCriteria(self, criteria): | ||
self.array.append(criteria) | ||
|
||
def __str__(self): | ||
output = "" | ||
for iCriteria in range(len(self.array)): | ||
output += (self.array[iCriteria]).show(iCriteria) + "&" | ||
return output[:len(output)-1] | ||
|
||
class criteria: | ||
def __init__(self, field, searchType, value, link="AND"): | ||
self.link = link | ||
self.field = field | ||
self.searchtype = searchType | ||
self.value = value | ||
|
||
def show(self, i): | ||
sep = "criteria[" + str(i) + "]" | ||
return sep + "[link]=" + self.link + "&" + sep + "[field]=" + self.field + "&" + sep + "[searchtype]=" + self.searchtype + "&" + sep + "[value]=" + self.value | ||
|
||
|
||
|
||
# Class used to create a ticket in the glpi api | ||
class glpiTicket: | ||
def __init__(self, **fields): | ||
self.__dict__ = fields | ||
|
||
def __init__(self, name, content, status=1, urgency=3, impact=3, priority=3): | ||
self.name = name | ||
self.content = content | ||
self.status = status | ||
self.urgency = urgency | ||
self.impact = impact | ||
self.priority = priority | ||
# self.entities_id = entities_id | ||
# self.requesttypes_id = requesttypes_id | ||
# self.itilcategories_id = itilcategories_id | ||
# self.type = type | ||
# self.global_validation = global_validation | ||
|
||
def build(self): | ||
return { key:value for key, value in self.__dict__.items() if not key.startswith('__') and not callable(key) } | ||
|
||
class glpiSession: | ||
def __init__(self, userToken, apiToken, baseUrl): | ||
self.userToken = userToken | ||
self.apiToken = apiToken | ||
self.baseUrl = baseUrl | ||
self.headers = '' | ||
self.sessionToken = '' | ||
self.field = { | ||
"content": "21", | ||
"status": "12", | ||
"name": "1", | ||
"type": "14" | ||
} | ||
self.status = { | ||
"assigned": "2", | ||
"planned": "3", | ||
"pending": "4", | ||
"solved": "5", | ||
"closed": "6", | ||
"new": "1" | ||
} | ||
|
||
def initSession(self): | ||
headers = { 'Content-Type': 'application/json', 'Authorization': "user_token "+self.userToken, 'App-Token': self.apiToken } | ||
response = requests.get(self.baseUrl + '/initSession', headers=headers) | ||
if (self.controlCode(['200'], response)): | ||
self.sessionToken = response.json()['session_token'] | ||
self.headers = { 'Content-Type': 'application/json', 'Session-Token': self.sessionToken, 'App-Token': self.apiToken } | ||
|
||
def killSession(self): | ||
response = requests.get(self.baseUrl + '/killSession', headers=self.headers) | ||
self.controlCode(['200'], response) | ||
exit() | ||
|
||
def openTicket(self, name, description): | ||
data = { "input": glpiTicket(name, description).build() } | ||
response = requests.post(self.baseUrl + '/Ticket' , headers=self.headers, data=json.dumps(data)) | ||
if (self.controlCode(['201'], response)): | ||
return 0 | ||
|
||
def getTickets(self): | ||
response = requests.get(self.baseUrl + '/Ticket' , headers=self.headers) | ||
if (self.controlCode(['200'], response)): | ||
return response.json() | ||
|
||
def getTicket(self, ticketID): | ||
response = requests.get(self.baseUrl + '/Ticket/' + ticketID , headers=self.headers) | ||
if (self.controlCode(['200'], response)): | ||
return response.json() | ||
|
||
def searchTicket(self, name, content=None, ticket_status=None): | ||
filters = criterias() | ||
# Filter on Name | ||
nameCriteria = criteria(self.field['name'], "contains", name, link="AND") | ||
filters.addCriteria(nameCriteria) | ||
# Filter on Description | ||
if (content != None): | ||
contentCriteria = criteria(self.field['content'], "contains", content, link="AND") | ||
filters.addCriteria(contentCriteria) | ||
# Filter on ticket_status | ||
if (ticket_status != None): | ||
statusCriteria = criteria(self.field['status'], "equals", self.status[ticket_status], link="AND") | ||
filters.addCriteria(statusCriteria) | ||
|
||
response = requests.get(self.baseUrl + '/search/Ticket?' + str(filters), headers=self.headers) | ||
if (self.controlCode(['200'], response)): | ||
return response.json() | ||
|
||
def similarTicketExists(self, ticket, ticket_status=None): | ||
if (ticket_status != None): | ||
for iStatus in ticket_status: | ||
if (self.searchTicket(ticket.name, ticket.content, iStatus)['totalcount'] >= 1): | ||
return True | ||
return False | ||
else: | ||
if (self.searchTicket(ticket.name, ticket.content, ticket_status)['totalcount'] >= 1): | ||
return True | ||
else: | ||
return False | ||
|
||
def customGETRequest(self, url): | ||
response = requests.get(self.baseUrl + url , headers=self.headers) | ||
if (self.controlCode(['200'], response)): | ||
return (response.text.encode('utf-8')) | ||
|
||
def controlCode(self, expectedCodes, response): | ||
if (str(response.status_code) in expectedCodes): | ||
return True | ||
else: | ||
print(response.text.encode('utf-8')) | ||
print("Something went wrong while calling the GLPI API, received error code " + str(response.status_code)) | ||
self.killSession() | ||
return False | ||
|
||
|
||
|
||
#userToken="2XVW2uSJXHlADythfFKmJzHlNYu5JSvcBsI4UoUB" | ||
#apiToken="2IaJezu1IfuPFAjfMTV0ypqFFjiKXyhROnaoMNp0" | ||
#a = glpiSession(userToken, apiToken, "http://192.168.180.99/glpi/apirest.php") | ||
#a.initSession() | ||
#print(json.dumps(a.getTickets())) | ||
#print(a.customGETRequest("/listSearchOptions/Ticket")) | ||
#ticket = glpiTicket("test8", "seg") | ||
#a.similarTicketExists(ticket) | ||
#print(json.dumps(a.searchTicket("[Rudder]test command"))) | ||
##a.getTickets() | ||
##a.openTicket("test1", "description1") | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
LOG_FILE = "/root/fda/notify.log" | ||
|
||
def log(message): | ||
f = open(LOG_FILE, "a") | ||
f.write(message) | ||
f.close() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import requests, urllib3, json, configparser, log_tail, os, re, time, glpi, traceback, glpi_logger | ||
|
||
class NotifyWorker: | ||
|
||
def __init__(self, pipefile, conf): | ||
self.fifo_pipe = pipefile | ||
self.conf = conf | ||
self.notif_queue = [] | ||
self.timestamp = int(time.time()) | ||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) | ||
self.glpi = None | ||
try: | ||
os.mkfifo(self.fifo_pipe) | ||
except OSError as e: | ||
if str(e).find("File exists") < 0: | ||
raise | ||
|
||
# This function reads non-compliance data from a pipe. | ||
# It is meant to be used with the Rudder server non-compliance hooks. | ||
def run(self): | ||
while True: | ||
if self.timestamp % 60 == 0: # This will check if there are unsent mails in the queue, and send them if the batch period is elapsed | ||
self.notify_mail() | ||
with open(self.fifo_pipe) as pipe: | ||
self.handle_non_compliance(pipe.read()) | ||
|
||
# This function reads its input from the Rudder server non-compliance logs directly. | ||
# It is a temporary solution, used until the server's non-compliance hooks are implemented. | ||
def run_with_logtail(self): | ||
for line in log_tail.log_tail("/var/log/rudder/compliance/non-compliant-reports.log"): | ||
self.handle_non_compliance(line) | ||
|
||
def start(self): | ||
#self.run() | ||
glpi_logger.log("Starting watch watch\n") | ||
self.run_with_logtail() | ||
|
||
def handle_non_compliance(self, msg_str): | ||
try: | ||
msg = Message(msg_str) | ||
glpi_logger.log("parsing line: " + msg_str + "\n") | ||
self.notif_queue.append(msg) | ||
if self.conf["MAIL"]["on"] == "true": | ||
self.notify_mail() | ||
if self.conf["SLACK"]["on"] == "true": | ||
self.notify_slack(msg) | ||
if self.conf["GLPI"]["on"] == "true": | ||
self.notify_glpi(msg) | ||
except Exception as e: | ||
glpi_logger.log("/!\ something went wrong:" + str(e)) | ||
glpi_logger.log(traceback.print_exc()) | ||
|
||
|
||
def notify_slack(self, msg): | ||
for webhook in self.conf["SLACK"]["webhooks"].split(' '): | ||
requests.post(webhook, headers={"Content-type":"application/json"}, data=json.dumps({ | ||
"text":"*RUDDER NON-COMPLIANCE*\n" + str(msg) | ||
})) | ||
|
||
def notify_glpi(self, msg): | ||
glpi_logger.log(" -- notify via glpi --\n") | ||
if (self.glpi == None): | ||
userToken="2XVW2uSJXHlADythfFKmJzHlNYu5JSvcBsI4UoUB" | ||
apiToken="2IaJezu1IfuPFAjfMTV0ypqFFjiKXyhROnaoMNp0" | ||
newSession = glpi.glpiSession(userToken, apiToken, "http://192.168.180.99/glpi/apirest.php") | ||
glpi_logger.log(" -- initializing session --\n") | ||
newSession.initSession() | ||
glpi_logger.log(" -- done --\n") | ||
self.glpi = newSession | ||
|
||
# Do not parse repaired reports or logs | ||
if (msg.getResultStatus() != "result_repaired" and msg.getResultStatus() != "log_repaired" and msg.getResultStatus() != "log_warn"): | ||
for iFile in self.conf['GLPI']['files'].split(' '): | ||
ticketName = "[Rudder]" + msg.data['directive_name'] | ||
ticketContent = str(msg) | ||
lookedContent = msg.withoutTimeStamp() | ||
lookedStatus = ['new', 'assigned', 'planned', 'pending'] | ||
|
||
glpi_logger.log("looking for ticket: " + ticketName + "\n with content" + lookedContent + "\n") | ||
sampleTicket = glpi.glpiTicket(ticketName, lookedContent) | ||
if not self.glpi.similarTicketExists(sampleTicket, lookedStatus): | ||
glpi_logger.log(" -- creating ticket --") | ||
self.glpi.openTicket(ticketName, ticketContent) | ||
glpi_logger.log(" -- done --\n\n") | ||
else: | ||
glpi_logger.log(" -- SKIPPING, ticket already exists --\n\n") | ||
else: | ||
glpi_logger.log(" -- SKIPPING, repaired report --\n\n") | ||
|
||
def notify_mail(self): | ||
if self.conf["MAIL"]["nospam"] == "true" and time.time() - self.timestamp < self.get_mail_batch_period(): | ||
return | ||
# The code above is enabling batch mail-sending by checking if the configured period has elapsed since last mail | ||
|
||
tmp_mailfile = "/tmp/rudder-notify-mail.txt" | ||
with open(tmp_mailfile, 'a') as out: | ||
out.write(str(len(self.notif_queue)) + " notification" + ('s' if len(self.notif_queue) > 1 else '') + " from Rudder :\n") | ||
for i in range(0, len(self.notif_queue)): | ||
out.write("# " + str(i+1) + ' :\n' + str(self.notif_queue[i]) + '\n') | ||
self.notif_queue = [] | ||
for recipient in self.conf["MAIL"]["recipients"].split(' '): | ||
os.system("mail -s 'Rudder non-compliance notification' " + recipient + " < " + tmp_mailfile) | ||
os.remove(tmp_mailfile) | ||
self.timestamp = time.time() # And we reset the timestamp | ||
|
||
def get_mail_batch_period(self): | ||
regex = re.compile("([0-9]*)d?([0-9]*)h?([0-9]*)m?") | ||
g = map(int, regex.search(self.conf["MAIL"]["batch_period"]).groups()) | ||
return 86400 * g[0] + 3600 * g[1] + 60 * g[2] | ||
|
||
class Message: | ||
|
||
def __init__(self, msg): | ||
regex = re.compile("^\[(?P<Date>[^\]]+)\] N: (?P<NodeUUID>[^ ]+) \[(?P<NodeFQDN>[^\]]+)\] S: \[(?P<Result>[^\]]+)\] R: (?P<RuleUUID>[^ ]+) \[(?P<RuleName>[^\]]+)\] D: (?P<DirectiveUUID>[^ ]+) \[(?P<DirectiveName>[^\]]+)\] T: (?P<TechniqueName>[^/]+)/(?P<TechniqueVersion>[^ ]+) C: \[(?P<ComponentName>[^\]]+)\] V: \[(?P<ComponentKey>[^\]]+)\] (?P<Message>.+)$") | ||
groups = regex.search(msg).groups() | ||
self.data = { | ||
"date": groups[0], | ||
"node_uuid": groups[1], | ||
"node_fqdn": groups[2], | ||
"result": groups[3], | ||
"rule_uuid": groups[4], | ||
"rule_name": groups[5], | ||
"directive_uuid": groups[6], | ||
"directive_name": groups[7], | ||
"technique_name": groups[8], | ||
"technique_version": groups[9], | ||
"component_name": groups[10], | ||
"component_key": groups[11], | ||
"message": groups[12] | ||
} | ||
|
||
def __str__(self): | ||
return " - Date: " + self.data["date"] + \ | ||
"\n - Node UUID: " + self.data["node_uuid"] + \ | ||
"\n - Node FQDN: " + self.data["node_fqdn"] + \ | ||
"\n - Result: " + self.data["result"] + \ | ||
"\n - Rule UUID: " + self.data["rule_uuid"] + \ | ||
"\n - Rule name: " + self.data["rule_name"] + \ | ||
"\n - Directive UUID: " + self.data["directive_uuid"] + \ | ||
"\n - Directive name: " + self.data["directive_name"] + \ | ||
"\n - Technique name: " + self.data["technique_name"] + \ | ||
"\n - Technique version: " + self.data["technique_version"] + \ | ||
"\n - Component name: " + self.data["component_name"] + \ | ||
"\n - Component key: " + self.data["component_key"] + \ | ||
"\n - Message: " + self.data["message"] + "\n" | ||
|
||
def withoutTimeStamp(self): | ||
return self.__str__().split("\n")[1] | ||
|
||
def getResultStatus(self): | ||
return self.data['result'] | ||
|
Oops, something went wrong.