diff --git a/docs/1-Using-Fleet/standard-query-library/standard-query-library.yml b/docs/1-Using-Fleet/standard-query-library/standard-query-library.yml index 701181868..9a63f40eb 100644 --- a/docs/1-Using-Fleet/standard-query-library/standard-query-library.yml +++ b/docs/1-Using-Fleet/standard-query-library/standard-query-library.yml @@ -17,6 +17,7 @@ spec: description: Retrieves the OpenSSL version. query: SELECT name AS name, version AS version, 'deb_packages' AS source FROM deb_packages WHERE name LIKE 'openssl%' UNION SELECT name AS name, version AS version, 'apt_sources' AS source FROM apt_sources WHERE name LIKE 'openssl%' UNION SELECT name AS name, version AS version, 'rpm_packages' AS source FROM rpm_packages WHERE name LIKE 'openssl%'; purpose: Detection + contributors: zwass --- apiVersion: v1 kind: query @@ -26,6 +27,7 @@ spec: description: Gatekeeper tries to ensure only trusted software is run on a mac machine. query: SELECT * FROM gatekeeper WHERE assessments_enabled = 0; purpose: Detection + contributors: zwass --- apiVersion: v1 kind: query @@ -36,6 +38,7 @@ spec: query: SELECT username, authorized_keys. * FROM users CROSS JOIN authorized_keys USING (uid); purpose: Detection remediation: Check out the linked table (https://github.com/fleetdm/fleet/blob/32b4d53e7f1428ce43b0f9fa52838cbe7b413eed/handbook/queries/detect-hosts-with-high-severity-vulnerable-versions-of-openssl.md#table-of-vulnerable-openssl-versions) to determine if the installed version is a high severity vulnerability and view the corresponding CVE(s) + contributors: mike-j-thomas --- apiVersion: v1 kind: query @@ -65,6 +68,7 @@ spec: description: Retrieve application, system, and mobile app crash logs. query: SELECT uid, datetime, responsible, exception_type, identifier, version, crash_path FROM users CROSS JOIN crashes USING (uid); purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -74,6 +78,7 @@ spec: description: List installed Chrome Extensions for all users. query: SELECT uid, datetime, responsible, exception_type, identifier, version, crash_path FROM users CROSS JOIN crashes USING (uid); purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -83,6 +88,7 @@ spec: description: Get all software installed on a FreeBSD computer, including browser plugins and installed packages. Note, this does not included other running processes in the processes table. query: SELECT name AS name, version AS version, 'Browser plugin (Chrome)' AS type, 'chrome_extensions' AS source FROM chrome_extensions UNION SELECT name AS name, version AS version, 'Browser plugin (Firefox)' AS type, 'firefox_addons' AS source FROM firefox_addons UNION SELECT name AS name, version AS version, 'Package (Atom)' AS type, 'atom_packages' AS source FROM atom_packages UNION SELECT name AS name, version AS version, 'Package (Python)' AS type, 'python_packages' AS source FROM python_packages UNION SELECT name AS name, version AS version, 'Package (pkg)' AS type, 'pkg_packages' AS source FROM pkg_packages; purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -92,6 +98,7 @@ spec: description: Get the installed homebrew package database. query: SELECT * FROM homebrew_packages; purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -101,6 +108,7 @@ spec: description: Get all software installed on a Linux computer, including browser plugins and installed packages. Note, this does not included other running processes in the processes table. query: SELECT name AS name, version AS version, 'Package (APT)' AS type, 'apt_sources' AS source FROM apt_sources UNION SELECT name AS name, version AS version, 'Package (deb)' AS type, 'deb_packages' AS source FROM deb_packages UNION SELECT package AS name, version AS version, 'Package (Portage)' AS type, 'portage_packages' AS source FROM portage_packages UNION SELECT name AS name, version AS version, 'Package (RPM)' AS type, 'rpm_packages' AS source FROM rpm_packages UNION SELECT name AS name, '' AS version, 'Package (YUM)' AS type, 'yum_sources' AS source FROM yum_sources UNION SELECT name AS name, version AS version, 'Package (NPM)' AS type, 'npm_packages' AS source FROM npm_packages UNION SELECT name AS name, version AS version, 'Package (Atom)' AS type, 'atom_packages' AS source FROM atom_packages UNION SELECT name AS name, version AS version, 'Package (Python)' AS type, 'python_packages' AS source FROM python_packages; purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -110,6 +118,7 @@ spec: description: Get all software installed on a macOS computer, including apps, browser plugins, and installed packages. Note, this does not included other running processes in the processes table. query: SELECT name AS name, bundle_short_version AS version, 'Application (macOS)' AS type, 'apps' AS source FROM apps UNION SELECT name AS name, version AS version, 'Package (Python)' AS type, 'python_packages' AS source FROM python_packages UNION SELECT name AS name, version AS version, 'Browser plugin (Chrome)' AS type, 'chrome_extensions' AS source FROM chrome_extensions UNION SELECT name AS name, version AS version, 'Browser plugin (Firefox)' AS type, 'firefox_addons' AS source FROM firefox_addons UNION SELECT name As name, version AS version, 'Browser plugin (Safari)' AS type, 'safari_extensions' AS source FROM safari_extensions UNION SELECT name AS name, version AS version, 'Package (Homebrew)' AS type, 'homebrew_packages' AS source FROM homebrew_packages; purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -119,6 +128,7 @@ spec: description: Retrieves the list of installed Safari Extensions for all users in the target system. query: SELECT safari_extensions.* FROM users join safari_extensions USING (uid); purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -128,6 +138,7 @@ spec: description: Get all software installed on a Windows computer, including programs, browser plugins, and installed packages. Note, this does not included other running processes in the processes table. query: SELECT name AS name, version AS version, 'Program (Windows)' AS type, 'programs' AS source FROM programs UNION SELECT name AS name, version AS version, 'Package (Python)' AS type, 'python_packages' AS source FROM python_packages UNION SELECT name AS name, version AS version, 'Browser plugin (IE)' AS type, 'ie_extensions' AS source FROM ie_extensions UNION SELECT name AS name, version AS version, 'Browser plugin (Chrome)' AS type, 'chrome_extensions' AS source FROM chrome_extensions UNION SELECT name AS name, version AS version, 'Browser plugin (Firefox)' AS type, 'firefox_addons' AS source FROM firefox_addons UNION SELECT name AS name, version AS version, 'Package (Chocolatey)' AS type, 'chocolatey_packages' AS source FROM chocolatey_packages UNION SELECT name AS name, version AS version, 'Package (Atom)' AS type, 'atom_packages' AS source FROM atom_packages UNION SELECT name AS name, version AS version, 'Package (Python)' AS type, 'python_packages' AS source FROM python_packages; purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -137,6 +148,7 @@ spec: description: query: SELECT * FROM battery WHERE health != 'Good' AND condition NOT IN ('', 'Normal'); purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -146,6 +158,7 @@ spec: description: Displays the percentage of free space available on the primary disk partition. query: SELECT (blocks_available * 100 / blocks) AS pct, * FROM mounts WHERE path = '/'; purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -155,6 +168,7 @@ spec: description: Shows system mounted devices and filesystems (not process specific). query: SELECT device, device_alias, path, type, blocks_size FROM mounts; purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -164,6 +178,7 @@ spec: description: Shows system mounted devices and filesystems (not process specific). query: SELECT * FROM os_version; purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -173,6 +188,7 @@ spec: description: Shows information about the host platform query: SELECT vendor, version, date, revision from platform_info; purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -182,6 +198,7 @@ spec: description: Shows applications and binaries set as user/login startup items. query: SELECT * FROM startup_items; purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -191,6 +208,7 @@ spec: description: Get a list of system logins and logouts. query: SELECT * FROM last; purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -210,6 +228,7 @@ spec: description: Shows the system uptime. query: SELECT * FROM uptime; purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -219,6 +238,7 @@ spec: description: Shows all USB devices that are actively plugged into the host system. query: SELECT * FROM usb_devices; purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -228,6 +248,7 @@ spec: description: Shows information about the wifi network that a host is currently connected to. query: SELECT * FROM wifi_status; purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -237,6 +258,7 @@ spec: description: query: SELECT * FROM bitlocker_info WHERE protection_status = 0; purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query @@ -288,6 +310,7 @@ spec: description: suid binaries in common locations. query: SELECT * FROM suid_bin; purpose: Informational + contributors: zwass --- apiVersion: v1 kind: query diff --git a/website/assets/images/icon-search-16x16@2x.png b/website/assets/images/icon-search-16x16@2x.png index fb6d1bb3d..ae626c723 100644 Binary files a/website/assets/images/icon-search-16x16@2x.png and b/website/assets/images/icon-search-16x16@2x.png differ diff --git a/website/assets/js/pages/query-detail.page.js b/website/assets/js/pages/query-detail.page.js index dc4010d7d..5644da4a4 100644 --- a/website/assets/js/pages/query-detail.page.js +++ b/website/assets/js/pages/query-detail.page.js @@ -13,35 +13,19 @@ parasails.registerPage('query-detail', { //… }, mounted: async function () { - if (this.query && this.query.contributors) { - this.contributors = await Promise.all( - this.query.contributors - .split(',') - .map(async (contributor) => this.getGitHubUserData(contributor)) - ); - } }, // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ methods: { - getGitHubUserData: async function (userName) { - const url = - 'https://api.github.com/users/' + encodeURIComponent(userName); - return await fetch(url, { - method: 'GET', - headers: { - Accept: 'application/vnd.github.v3+json', - }, - }) - .then((response) => response.json()) - .catch((error) => console.log(error)); - }, clickAvatar: function (contributor) { - window.location = contributor.html_url; + window.location = contributor.htmlUrl; }, + getDisplayName: function (contributor) { + return !contributor.name ? contributor.handle : contributor.name; + } }, }); diff --git a/website/assets/js/pages/query-library.page.js b/website/assets/js/pages/query-library.page.js index 12a9303fe..b372ca9cc 100644 --- a/website/assets/js/pages/query-library.page.js +++ b/website/assets/js/pages/query-library.page.js @@ -3,12 +3,11 @@ parasails.registerPage('query-library', { // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ data: { - contributorsDictionary: {}, inputTextValue: '', inputTimers: {}, searchString: '', // The user input string to be searched against the query library - selectedPurpose: 'all', // Initially set to all, the user may select a different option to filter queries by purpose (e.g., "all queries", "information", "detection") - selectedPlatform: 'all', // Initially set to all, the user may select a different option to filter queries by platform (e.g., "all platforms", "macOS", "Windows", "Linux") + selectedPurpose: 'all queries', // Initially set to all, the user may select a different option to filter queries by purpose (e.g., "all queries", "information", "detection") + selectedPlatform: 'all platforms', // Initially set to all, the user may select a different option to filter queries by platform (e.g., "all platforms", "macOS", "Windows", "Linux") }, computed: { @@ -36,43 +35,50 @@ parasails.registerPage('query-library', { //… }, mounted: async function () { - const uniqueContributors = this._getUniqueContributors(this.queries); - this.contributorsDictionary = Object.assign( - {}, - await this._threadGitHubAPICalls(uniqueContributors) - ); + //… }, // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ methods: { + clickSelectPurpose(purpose) { + this.selectedPurpose = purpose; + }, + + clickSelectPlatform(platform) { + this.selectedPlatform = platform; + }, + clickCard: function (querySlug) { window.location = '/queries/' + querySlug; // we can trust the query slug is url-safe }, clickAvatar: function (contributor) { - window.location = contributor.html_url; + window.location = contributor.htmlUrl; }, getAvatarUrl: function (contributorData) { - return contributorData ? contributorData.avatar_url : ''; + return contributorData ? contributorData.avatarUrl : ''; }, - getContributorsString: function (list, dictionary) { + getContributorsString: function (contributors) { + if (!contributors) { + return; + } const displayName = (contributorData) => { if (contributorData) { return !contributorData.name - ? contributorData.login + ? contributorData.handle : contributorData.name; } }; - let contributorString = displayName(dictionary[list[0]]); - if (list.length > 2) { - contributorString += ` and ${list.length - 1} others`; + let contributorString = displayName(contributors[0]); + if (contributors.length > 2) { + contributorString += ` and ${contributors.length - 1} others`; } - if (list.length === 2) { - contributorString += ` and ${displayName(dictionary[list[1]])}`; + if (contributors.length === 2) { + contributorString += ` and ${displayName(contributors[1])}`; } return contributorString; }, @@ -90,15 +96,22 @@ parasails.registerPage('query-library', { this.searchString = this.inputTextValue; }, - _search: function (library, searchString) { - const searchTerms = _.isString(searchString) - ? searchString.toLowerCase().split(' ') - : []; - return library.filter((item) => { - const description = _.isString(item.description) - ? item.description.toLowerCase() - : ''; - return searchTerms.some((term) => description.includes(term)); + _search: function (queries, searchString) { + if (_.isEmpty(searchString)) { + return queries; + } + + const normalize = (value) => _.isString(value) ? value.toLowerCase() : ''; + const searchTerms = normalize(searchString).split(' '); + + return queries.filter((query) => { + let textToSearch = normalize(query.name) + ', ' + normalize(query.description); + if (query.contributors) { + query.contributors.forEach((contributor) => { + textToSearch += ', ' + normalize(contributor.name) + ', ' + normalize(contributor.handle); + }); + } + return (searchTerms.some((term) => textToSearch.includes(term))); }); }, @@ -114,57 +127,6 @@ parasails.registerPage('query-library', { ); }, - _threadGitHubAPICalls: async function (contributorsList) { - // create threads object with a thread for each contributor each thread is a promise that will resolve - // when the async call to the GitHub API resolves for that contributor - const threads = contributorsList.reduce((threads, contributor) => { - threads[contributor] = this._getGitHubUserData(contributor); - return threads; - }, {}); - - // each thread resolves with a key-value pair where the key is the contributor's GitHub handle and the value - // is the deserialized JSON response returned by the GitHub API for that contributor - const resolvedThreads = await Promise.all( - Object.keys(threads).map((key) => - Promise.resolve(threads[key]).then((result) => ({ [key]: result })) - ) - ).then((resultsArray) => { - const resolvedThreads = resultsArray.reduce( - (resolvedThreads, result) => { - Object.assign(resolvedThreads, result); - return resolvedThreads; - }, - {} - ); - return resolvedThreads; - }); - return resolvedThreads; - }, - - _getUniqueContributors: function (queries) { - return queries.reduce((uniqueContributors, query) => { - if (query.contributors) { - uniqueContributors = _.union( - uniqueContributors, - query.contributors.split(',') - ); - } - return uniqueContributors; - }, []); - }, - - _getGitHubUserData: async function (gitHubHandle) { - const url = - 'https://api.github.com/users/' + encodeURIComponent(gitHubHandle); - const userData = await fetch(url, { - method: 'GET', - headers: { - Accept: 'application/vnd.github.v3+json', - }, - }) - .then((response) => response.json()) - .catch(() => {}); - return userData; - }, }, + }); diff --git a/website/assets/styles/bootstrap-overrides.less b/website/assets/styles/bootstrap-overrides.less index c03b5974b..e8e310f1b 100644 --- a/website/assets/styles/bootstrap-overrides.less +++ b/website/assets/styles/bootstrap-overrides.less @@ -125,9 +125,6 @@ a.text-danger:hover, a.text-danger:focus { box-shadow: 0px 6px 20px rgba(0, 0, 0, 0.05); } -.dropdown:hover > .dropdown-menu { - display: block; -} .dropdown:hover > .btn { color: #6a67fe; } diff --git a/website/assets/styles/layout.less b/website/assets/styles/layout.less index d47c780d4..31a59f56d 100644 --- a/website/assets/styles/layout.less +++ b/website/assets/styles/layout.less @@ -20,15 +20,15 @@ html, body { position: absolute; left: 0; right: 0; + .dropdown:hover > .dropdown-menu { + display: block; + } .header-btn { color: #ffffff; cursor: unset; font-family: @navigation-font; font-weight: @bold; } - // .header-btn[aria-expanded='true'] { - // color: #6a67fe; - // } .header-link { color: #ffffff; font-weight: @bold; @@ -104,6 +104,9 @@ html, body { left: 0; right: 0; border-bottom: 1px solid #e2e4ea; + .dropdown:hover > .dropdown-menu { + display: block; + } .header-btn { color: #192147; cursor: unset; diff --git a/website/assets/styles/pages/query-detail.less b/website/assets/styles/pages/query-detail.less index 0f9dc087b..d1ad337d2 100644 --- a/website/assets/styles/pages/query-detail.less +++ b/website/assets/styles/pages/query-detail.less @@ -71,10 +71,10 @@ .platforms, .purpose, .contributors, .contribute { min-height: 36px; + line-height: 24px; p, a { font-family: 'Nunito'; font-size: 16px; - line-height: 24px; } } @@ -89,6 +89,19 @@ } } + @media (max-width: 700px) { + + .contribute { + flex-wrap: wrap; + min-height: 24px; + h5 { + padding: 6px 0px 6px 0px; + } + } + + } + + @media (max-width: 575px) { h2 { diff --git a/website/assets/styles/pages/query-library.less b/website/assets/styles/pages/query-library.less index ba8d336d7..0562cd9dd 100644 --- a/website/assets/styles/pages/query-library.less +++ b/website/assets/styles/pages/query-library.less @@ -1,5 +1,4 @@ #query-library { - h2 { padding: 0px 30px 0px 30px; } @@ -14,7 +13,6 @@ font-size: 16px; line-height: 22px; padding: 0px 30px 0px 30px; - } a { @@ -27,24 +25,83 @@ height: 16px; width: 16px; } + &.search { + transform: scale(0.5); + } } input { + &::placeholder { + font-size: 16px; + line-height: 24px; + } + } + + .input-group { + &.search { + width: 250px; + } + } + + .input-group-text { + color: #8b8fa2; + border-color: #c5c7d1; + border-top-left-radius: 8px; + border-bottom-left-radius: 8px; + } + + .form-control { + font-size: 16px; + border-color: #c5c7d1; + border-top-right-radius: 8px; + border-bottom-right-radius: 8px; + &:focus { + border: 1px solid #c5c7d1; + } + } + + .btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus, .show > .btn-secondary.dropdown-toggle:focus { + box-shadow: none; + } + + .btn-secondary { + font-family: Nunito; + color: @core-vibrant-blue; + background-color: transparent; + border: 0; + cursor: pointer; + &:focus { + border: 0; + box-shadow: none; + } + } + + .filters { height: 54px; - width: 250px; - border: 1px solid #C5C7D1; + p { + font-family: Nunito; + font-size: 16px; + line-height: 25px; + } + } + + .dropdown-menu { border-radius: 8px; - padding: 15px; - &.mobile { - width: 100%; - margin-right: 0; - margin-left: 0; + cursor: pointer; + } + .dropdown-item { + border-radius: 8px; + cursor: pointer; + &:hover { + background-color: lightness(@core-vibrant-blue, 10%); + margin-right: 15px; } } select { color: @core-vibrant-blue; border: 0px; + outline: 0; &:focus { border: 0px; } @@ -65,7 +122,9 @@ } .library { - max-width: 860px; + max-width: 960px; + margin-top: 80px; + margin-bottom: 0; } .description { @@ -81,10 +140,10 @@ width: 100%; margin-right: 0; margin-left: 0; - border: 1px solid #C5C7D1; + border: 1px solid #c5c7d1; border-radius: 8px; padding-right: 15px; -} + } .select-mobile { padding-left: 30px; @@ -101,10 +160,11 @@ .filter-and-search-bar { padding-left: 45px; padding-right: 45px; + margin-bottom: 0; + min-height: 54px; } .contributors, .platforms { - p { font-size: 13px; line-height: 20px; @@ -120,7 +180,7 @@ margin-left: 30px; margin-right: 30px; border-bottom: 1px solid; - border-color: #E2E4EA; + border-color: #e2e4ea; } .card.results { @@ -129,7 +189,7 @@ border-radius: 8px; &:hover { - background-color: #F1F0FF; + background-color: #f1f0ff; cursor: pointer; } } @@ -141,7 +201,6 @@ box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1); margin-bottom: 90px; width: 100%; - } .card-body { @@ -168,20 +227,36 @@ } } - @media (max-width: 575px) { + @media (max-width: 768px) { + .library { + max-width: 720px; + margin-top: 60px; + } + .results { + margin-top: 16px; + } + } + @media (max-width: 575px) { h2 { font-size: 28px; line-height: 36px; } - .contributors, .platforms { + .library { + max-width: none; + margin-bottom: 0px; + } + .results { + margin-top: 16px; + } + + .contributors, .platforms { p { font-size: 13px; line-height: 20px; } } } - } diff --git a/website/scripts/build-static-content.js b/website/scripts/build-static-content.js index b2611d451..5f53d8a24 100644 --- a/website/scripts/build-static-content.js +++ b/website/scripts/build-static-content.js @@ -29,6 +29,7 @@ module.exports = { let yaml = await sails.helpers.fs.read(path.join(topLvlRepoPath, RELATIVE_PATH_TO_QUERY_LIBRARY_YML_IN_FLEET_REPO)); let queriesWithProblematicRemediations = []; + let queriesWithProblematicContributors = []; let queries = YAML.parseAllDocuments(yaml).map((yamlDocument)=>{ let query = yamlDocument.toJSON().spec; query.slug = _.kebabCase(query.name);// « unique slug to use for routing to this query's detail page @@ -38,6 +39,12 @@ module.exports = { } else if (query.remediation === undefined) { query.remediation = 'N/A';// « We set this to a string here so that the data type is always string. We use N/A so folks can see there's no remediation and contribute if desired. } + + // GitHub usernames may only contain alphanumeric characters or single hyphens, and cannot begin or end with a hyphen. + if (!query.contributors || (query.contributors !== undefined && !_.isString(query.contributors)) || query.contributors.split(',').some((contributor) => contributor.match('^[^A-za-z0-9].*|[^A-Za-z0-9-]|.*[^A-za-z0-9]$'))) { + queriesWithProblematicContributors.push(query); + } + return query; }); // Report any errors that were detected along the way in one fell swoop to avoid endless resubmitting of PRs. @@ -48,6 +55,41 @@ module.exports = { if (queries.length !== _.uniq(_.pluck(queries, 'slug')).length) { throw new Error('Failed parsing YAML for query library: Queries as currently named would result in colliding (duplicate) slugs. To resolve, rename the queries whose names are too similar. Note the duplicates: ' + _.pluck(queries, 'slug').sort()); }//• + // Report any errors that were detected along the way in one fell swoop to avoid endless resubmitting of PRs. + if (queriesWithProblematicContributors.length >= 1) { + throw new Error('Failed parsing YAML for query library: The "contributors" of a query should be a single string of valid GitHub user names (e.g. "zwass", or "zwass,noahtalerman,mikermcneil"). But one or more queries have an invalid "contributors" value: ' + _.pluck(queriesWithProblematicContributors, 'slug').sort()); + }//• + + // Get a distinct list of all GitHub usernames from all of our queries. + // Map all queries to build a list of unique contributor names then build a dictionary of user profile information from the GitHub Users API + const githubUsernames = queries.reduce((list, query) => { + if (!queriesWithProblematicContributors.find((element) => element.slug === query.slug)) { + list = _.union(list, query.contributors.split(',')); + } + return list; + }, []); + + // Talk to GitHub and get additional information about each contributor. + let githubDataByUsername = {}; + await sails.helpers.flow.simultaneouslyForEach(githubUsernames, async(username)=>{ + githubDataByUsername[username] = await sails.helpers.http.get('https://api.github.com/users/' + encodeURIComponent(username), {}, { 'User-Agent': 'Fleet-Standard-Query-Library', Accept: 'application/vnd.github.v3+json' }); + });//∞ + + // Now expand queries with relevant profile data for the contributors. + for (let query of queries) { + let usernames = query.contributors.split(','); + let contributorProfiles = []; + for (let username of usernames) { + contributorProfiles.push({ + name: githubDataByUsername[username].name, + handle: githubDataByUsername[username].login, + avatarUrl: githubDataByUsername[username].avatar_url, + htmlUrl: githubDataByUsername[username].html_url, + }); + } + query.contributors = contributorProfiles; + } + // Attach to Sails app configuration. builtStaticContent.queries = queries; builtStaticContent.queryLibraryYmlRepoPath = RELATIVE_PATH_TO_QUERY_LIBRARY_YML_IN_FLEET_REPO; diff --git a/website/views/pages/query-detail.ejs b/website/views/pages/query-detail.ejs index a069b261c..6e02bb9cd 100644 --- a/website/views/pages/query-detail.ejs +++ b/website/views/pages/query-detail.ejs @@ -1,98 +1,109 @@
{{query.tip}}
-{{query.query}}
- {{query.remediation}}
-