custom/asf.js (368 lines of code) (raw):

// ASF Feature changelog from v4 to v5 (asf010 branch) // // - Fixed logic error in email sending button // - Email can only be sent if document validates with no errors // - Ability to edit the announcement email entry, it populates when CVE is allocated now (or if blank) // - Checks for bad URLs, correct use of apache.org vendor URLs in references // - If wrong URL /cve or /cve5 is used it will redirect // - Has a non-auth /publicjson/CVE-id endpoint to grab the (full) JSON of only public issues (not for public use, for ASF etc) // - Requirement for severity level // const { v4: uuidv4 } = require('uuid'); const request = require('request'); const express = require('express'); const conf = require('../config/conf'); const email = require('../customRoutes/email.js'); async function asfemaillists (req, res) { var emaillist = await new Promise( xres => { self.getemaillistforpmc(req.query.pmc, xres)}); res.send(emaillist) } async function asfpublicjsonlist(req, res) { let Document4 = res.locals.docs.cve.Document; var r4 = await Document4.aggregate([ { $match: { 'body.CVE_data_meta.STATE': 'PUBLIC' }}, { $project: { ID: '$body.CVE_data_meta.ID', title: '$body.CVE_data_meta.TITLE', state: '$body.CVE_data_meta.STATE', updated: '$updatedAt', owner: '$body.CNA_private.owner' }} ]); let Document5 = res.locals.docs.cve5.Document; var r5 = await Document5.aggregate([ { $match: { 'body.CNA_private.state': 'PUBLIC' }}, { $project: { ID: '$body.cveMetadata.cveId', title: '$body.containers.cna.title', state: '$body.CNA_private.state', updated: '$updatedAt', owner: '$body.CNA_private.owner' }} ]); res.json(r4.concat(r5)); } const nodoc = {"error":"nodoc"}; async function findCVE(Document, idField, id, cb) { var q = {}; q[idField] = id; Document.findOne(q, async function (err, docs) { if (err) { res.json(nodoc); } else { cb(docs); } }); } async function asfpublicjson(req, res) { var ids = req.params.id.match(RegExp('CVE-[0-9-]+', 'img')); if (!ids || !ids[0]) { res.json(nodoc) return; } findCVE( res.locals.docs.cve5.Document, "body.cveMetadata.cveId", ids[0], async function (docs) { if (docs && docs.body && docs.body.CNA_private && docs.body.CNA_private.state == "PUBLIC") { res.json(docs.body) } else { findCVE( res.locals.docs.cve.Document, "body.CVE_data_meta.ID", ids[0], async function (docs) { if (docs && docs.body && docs.body.CVE_data_meta && docs.body.CVE_data_meta.STATE && docs.body.CVE_data_meta.STATE == "PUBLIC") { res.json(docs.body) } else { res.json(nodoc) } }) } }) } function asflogout (req, res) { req.logout(); req.session.returnTo = null; res.redirect('/users/login'); } function asflogin (req, res) { sess = req.session; if (req.query.code) { const userinfo_endpoint= 'https://oauth.apache.org/token' uri = userinfo_endpoint+"?code="+req.query.code request(uri, {json:true},(err,cbres,body) => { if (err) {res.send(err);} else if (cbres.statusCode != 200) {res.send(body);} else if (body.state != sess.state) { res.send("auth is broken") } else { pmcs = body.pmcs; for (i=0; i< body.projects.length; i++) { if (!pmcs.includes(body.projects[i])) { // we're a committer to project, but not in the PMC if (conf.pmcswithsecurityemails.includes(body.projects[i])|| body.projects[i] == "security") { // but this project has a security list console.log("User "+body.uid+" is committer to "+body.projects[i]+" but not PMC, allowed"); pmcs.push(body.projects[i]); } else { console.log("User "+body.uid+" is committer to "+body.projects[i]+" but not PMC, ignored"); } } } sess.user = {username:body.uid, email:body.email, name:body.fullname, pmcs:pmcs}; //sess.user = {username:body.uid, email:body.email, name:body.fullname, pmcs:["airflow"]}; if (sess.returnTo) { res.redirect(req.session.returnTo); delete req.session.returnTo; } else { res.redirect("/"); } console.log(body); } }); } else { delete sess.user; sess.state = uuidv4(); const authorization_endpoint= 'https://oauth.apache.org/auth' redirecturl = authorization_endpoint+"?state="+sess.state+"&redirect_uri=https://"+req.get('host')+req.originalUrl; res.redirect(redirecturl) } } // If you are in security pmc allow you to specify a different pmc for testing function setpmc(req, res) { if (req.isAuthenticated()) { groups = req.user.pmcs; if (groups.includes(conf.admingroupname)) { if (req.query.pmc) { req.session.user.pmcs = req.query.pmc.split(','); res.json({"result":"ok"}); } else { res.json({"error":"no pmc given"}); } } else { res.json({"error":"you are not in "+conf.admingroupname+" pmc"}); } } } function usersmejson (req, res) { if (req.isAuthenticated()) { groups = req.user.pmcs; res.json({ default: req.user.email, value: req.user.email, }); } } function usersprofile (req,res) { user = req.user; user.group = user.pmcs; res.render('users/view', { title: 'Profile: ' + user.username, profile: user, admin: user.group.includes(conf.admingroupname), page: 'users', }); } function userslist (req,res) { res.render('blank'); } function cvenew (req,res,next) { // var pmcs = req.user.pmcs; // if (pmcs.includes(conf.admingroupname)) { // next(); // } else { res.redirect("/allocatecve"); // } } // If we are in security team then allow you to assign the CVE to any PMC // otherwise give a radio list of the PMCs you are part of function userslistjson (req, res) { if (req.isAuthenticated()) { groups = req.user.pmcs; if (groups && groups.includes(conf.admingroupname)) { res.json({ "description": "lower case pmc name to assign this to", "options": {"grid_columns":12}, }); } else { res.json({ enum: groups, format: "radio", options: {enum_titles: groups}, }); } } } var self = module.exports = { asfinit: function (app) { app.use(function (req, res, next) { if (req.session.user && req.session.user.username) { req.user = req.session.user } res.locals.docs = app.locals.docs; next(); }); }, asfroutes: function (ensureAuthenticated, app) { app.get("/users/login", asflogin); // replaces existing app.get("/users/logout", asflogout); // replaces existing app.get('/cve/new', ensureAuthenticated, cvenew); // replaces existing app.get('/cve5/new', ensureAuthenticated, cvenew); // replaces existing app.use('/.well-known', express.static("/opt/cveprocess/.well-known", { dotfiles: 'allow' } )); let ac = require('../customRoutes/allocatecve'); app.use('/allocatecve', ensureAuthenticated, ac.protected); let publishcve = require('../customRoutes/publishcve'); app.use('/publishcve', ensureAuthenticated, publishcve.protected); let semail = require('../customRoutes/sendemails'); app.use('/sendemails', ensureAuthenticated, semail.protected); app.get('/users/setpmc', ensureAuthenticated, setpmc); app.get('/users/me/json', ensureAuthenticated, usersmejson); app.get('/users/list/json', ensureAuthenticated, userslistjson); // replaces existing app.get('/users/list/', ensureAuthenticated, userslist); // replaces existing app.get('/users/profile/:id(' + conf.usernameRegex + ')?', ensureAuthenticated, usersprofile); // replaces existing app.get('/asfemaillists', ensureAuthenticated, asfemaillists); // work around CORS app.get('/publicjson', asfpublicjsonlist); app.get('/publicjson/:id', asfpublicjson); }, asfgroupacls: function (documentacl,yourpmcs) { //console.log('asf9 doc owner is '+documentacl+" and you are "+yourpmcs); if (yourpmcs.includes(conf.admingroupname)) { return true; } for (i=0; i< yourpmcs.length; i++) { if (yourpmcs[i] == documentacl) { return true; } } //console.log('asf9 access denied'); return false; }, // When a CVE record is changed this hook is called asfhookupsertdoc: function(req,dorefresh) { // in case we have an old record with no email list set CVE 5.0 if (req.body.CNA_private) { if (!req.body.CNA_private.userslist || req.body.CNA_private.userslist == "") { //self.getemaillistforpmc(req.body.CNA_private.owner,function(res) { // req.body.CNA_private.userslist = res; // dorefresh = true; // console.log(res); //}); } } // enforce workflow state cve4 if (req.body.CVE_data_meta) { // CVE 4.0 if (req.body.CVE_data_meta.STATE == "RESERVED") { // if it's in reserved but someone is editing it, move it to draft if (!req.user.pmcs.includes(conf.admingroupname)) { console.log("asf4 reserved but the description changed"); req.body.CVE_data_meta.STATE = "DRAFT"; dorefresh=true; } } } // enforce workflow state cve5 if (req.body.CNA_private && req.body.CNA_private.state) { // CVE 5.0 if (req.body.CNA_private.state == "RESERVED") { // if it's in reserved but someone is editing it, move it to draft if (!req.user.pmcs.includes(conf.admingroupname)) { console.log("asf4 RESERVED but the description changed"); req.body.CNA_private.state = "DRAFT"; dorefresh=true; } else { console.log("asf4 RESERVED but saved by security, no change"); } } } }, asfhookshowcveacl: function(doc, req, res) { if (!doc) { if (req._parsedOriginalUrl.query == 'r') { res.render('blank', { title: 'Error', }); return false; } req.flash('error',''); if (req.originalUrl.startsWith('/cve5/')) { res.redirect(req.originalUrl.replace("/cve5/","/cve/")+"?r"); } else { res.redirect(req.originalUrl.replace("/cve/","/cve5/")+"?r"); } return false; } if (doc && doc.body && doc.body.CNA_private && doc.body.CNA_private.owner) { if (!self.asfgroupacls(doc.body.CNA_private.owner, req.user.pmcs)) { req.flash('error','owned by pmc '+doc.body.CNA_private.owner); console.log("wrong acl"); doc = {}; res.render('blank', { title: 'Error', }); return false; } } else { req.flash('error','ACLs are bad tell security team "missing CNA_private.owner"'); res.render('blank', { title: 'Error', }); return false; } return true; }, // Send an email when someone adds a comment to a CVE asfhookaddcomment: function(doc,req) { var pathcve = "cve"; if (doc.body.cveMetadata && doc.body.cveMetadata.cveId) pathcve = "cve5"; var url = "https://"+req.client.servername+"/"+pathcve+"/"+req.body.id; se = email.sendemail({"from": "\""+req.user.name+"\" <"+req.user.email+">", "to": self.getsecurityemailaddress(doc.body.CNA_private.owner), "cc": "security@apache.org", "bcc": req.user.email, "subject":"Comment added on "+req.body.id, "text":req.body.plainText+"\n\n"+url}).then( (x) => { console.log("sent notification mail "+x);}); }, asfhookaddhistory: function(oldDoc, newDoc) { if (oldDoc != null) { if (newDoc.body.CVE_data_meta) { // CVE 4.0 if (newDoc.body.CVE_data_meta.STATE != oldDoc.body.CVE_data_meta.STATE) { console.log("asf4 changed state "+newDoc.body.CVE_data_meta.STATE); if (["REVIEW","READY","PUBLIC"].includes(newDoc.body.CVE_data_meta.STATE) || (newDoc.body.CVE_data_meta.STATE == "DRAFT" && oldDoc.body.CVE_data_meta.SATE == "REVIEW" )) { url = "https://cveprocess.apache.org/cve/"+newDoc.body.CVE_data_meta.ID; // hacky se = email.sendemail({"from": newDoc.author+"@apache.org", "cc":newDoc.author+"@apache.org", "subject":newDoc.body.CVE_data_meta.ID+" is now "+newDoc.body.CVE_data_meta.STATE, "text":newDoc.author+" changed state from "+oldDoc.body.CVE_data_meta.STATE+" to "+newDoc.body.CVE_data_meta.STATE+"\n\n"+url}).then( (x) => { console.log("sent notification mail "+x);}); } } } if (newDoc.body.CNA_private && newDoc.body.CNA_private.state) { // CVE 5.0 if (newDoc.body.CNA_private.state != oldDoc.body.CNA_private.state) { console.log("asf4 changed state "+newDoc.body.CNA_private.state); if (["REVIEW","READY","PUBLIC"].includes(newDoc.body.CNA_private.state) || (newDoc.body.CNA_private.state == "DRAFT" && oldDoc.body.CNA_private.state == "REVIEW" )) { url = "https://cveprocess.apache.org/cve5/"+newDoc.body.cveMetadata.cveId; // hacky se = email.sendemail({"from": newDoc.author+"@apache.org", "to":"security@apache.org", "cc":newDoc.author+"@apache.org", "subject":newDoc.body.cveMetadata.cveId+" is now "+newDoc.body.CNA_private.state, "text":newDoc.author+" changed state from "+oldDoc.body.CNA_private.state+" to "+newDoc.body.CNA_private.state+"\n\n"+url}).then( (x) => { console.log("sent notification mail "+x);}); } } } } }, asfallowedtodelete: function(req) { return req.user.pmcs.includes(conf.admingroupname); }, getsecurityemailaddress: function(pmc) { if (pmc == "security") { return "security@apache.org"; } if (conf.pmcswithsecurityemails.includes(pmc)) { return "security@"+pmc+".apache.org"; } else { return "private@"+pmc+".apache.org"; } }, getemaillistforpmc: function(pmc, cb) { var pmcfull = pmc+".apache.org" var listname = "dev@"+pmcfull; try { request('https://lists.apache.org/api/preferences.lua', {json:true},(err,cbres,body) => { if (err) {console.log(err);} else if (cbres.statusCode != 200) {console.log(cbres); } else { if (body.lists && body.lists[pmcfull]) { if (body.lists[pmcfull]["announce"]) { listname = "announce@"+pmcfull; } else if (body.lists[pmcfull]["announcements"]) { listname = "announcements@"+pmcfull; } else if (body.lists[pmcfull]["general"]) { listname = "general@"+pmcfull; } else if (body.lists[pmcfull]["users"]) { listname = "users@"+pmcfull; } else if (body.lists[pmcfull]["user"]) { listname = "user@"+pmcfull; } else if (body.lists[pmcfull]["discuss"]) { listname = "discuss@"+pmcfull; } else if (body.lists[pmcfull]["dev"]) { listname = "dev@"+pmcfull; } } } cb(listname); }); } catch (error) { console.log(error); cb(listname); } } }