diff --git a/website/assets/images/chevron-down-9x6@2x.png b/website/assets/images/chevron-down-9x6@2x.png new file mode 100644 index 0000000000..6750af9178 Binary files /dev/null and b/website/assets/images/chevron-down-9x6@2x.png differ diff --git a/website/assets/images/icon-search-16x16@2x.png b/website/assets/images/icon-search-16x16@2x.png new file mode 100644 index 0000000000..fb6d1bb3d0 Binary files /dev/null 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 e44781c79a..dc4010d7d1 100644 --- a/website/assets/js/pages/query-detail.page.js +++ b/website/assets/js/pages/query-detail.page.js @@ -3,23 +3,45 @@ parasails.registerPage('query-detail', { // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ data: { - //… + contributors: [], }, // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣ // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝ - beforeMount: function() { + beforeMount: function () { //… }, - mounted: async function() { - //… + 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; + }, + }, }); diff --git a/website/assets/js/pages/query-library.page.js b/website/assets/js/pages/query-library.page.js index 046e961d8f..12a9303fe4 100644 --- a/website/assets/js/pages/query-library.page.js +++ b/website/assets/js/pages/query-library.page.js @@ -3,6 +3,7 @@ parasails.registerPage('query-library', { // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ data: { + contributorsDictionary: {}, inputTextValue: '', inputTimers: {}, searchString: '', // The user input string to be searched against the query library @@ -12,57 +13,71 @@ parasails.registerPage('query-library', { computed: { filteredQueries: function () { - return _.filter(this.queries, (query) => this.isIncluded(query.platforms, this.selectedPlatform) && this.isIncluded(query.purpose, this.selectedPurpose)); + return this.queries.filter( + (query) => + this._isIncluded(query.platforms, this.selectedPlatform) && + this._isIncluded(query.purpose, this.selectedPurpose) + ); }, searchResults: function () { - return this.search(this.filteredQueries, this.searchString); + return this._search(this.filteredQueries, this.searchString); }, queriesList: function () { return this.searchResults; - } - + }, }, // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣ // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝ - beforeMount: function() { + beforeMount: function () { //… }, - mounted: async function() { - //… + mounted: async function () { + const uniqueContributors = this._getUniqueContributors(this.queries); + this.contributorsDictionary = Object.assign( + {}, + await this._threadGitHubAPICalls(uniqueContributors) + ); }, // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ methods: { + clickCard: function (querySlug) { + window.location = '/queries/' + querySlug; // we can trust the query slug is url-safe + }, - isIncluded: function (queryProperty, selectedOption) { - if (selectedOption.startsWith('all') || selectedOption === '') { - return true; + clickAvatar: function (contributor) { + window.location = contributor.html_url; + }, + + getAvatarUrl: function (contributorData) { + return contributorData ? contributorData.avatar_url : ''; + }, + + getContributorsString: function (list, dictionary) { + const displayName = (contributorData) => { + if (contributorData) { + return !contributorData.name + ? contributorData.login + : contributorData.name; + } + }; + let contributorString = displayName(dictionary[list[0]]); + if (list.length > 2) { + contributorString += ` and ${list.length - 1} others`; } - if (_.isArray(queryProperty)) { - queryProperty = queryProperty.join(', '); + if (list.length === 2) { + contributorString += ` and ${displayName(dictionary[list[1]])}`; } - return _.isString(queryProperty) && queryProperty.toLowerCase().includes(selectedOption.toLowerCase()); + return contributorString; }, - search: function (library, searchString) { - const searchTerms = _.isString(searchString) ? searchString.toLowerCase().split(' ') : []; - return library.filter((item) => { - const description = _.isString(item.description) ? item.description.toLowerCase() : ''; - return _.some(searchTerms, (term) => description.includes(term)); - }); - }, - - setSearchString: function () { - this.searchString = this.inputTextValue; - }, - - delayInput: function(callback, ms, label) { + delayInput: function (callback, ms, label) { let inputTimers = this.inputTimers; return function () { label = label || 'defaultTimer'; @@ -71,10 +86,85 @@ parasails.registerPage('query-library', { }; }, - clickCard: function (querySlug) { - window.location = '/queries/' + querySlug;// we can trust the query slug is url-safe + setSearchString: function () { + 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)); + }); + }, + _isIncluded: function (data, selectedOption) { + if (selectedOption.startsWith('all') || selectedOption === '') { + return true; + } + if (_.isArray(data)) { + data = data.join(', '); + } + return ( + _.isString(data) && data.toLowerCase().includes(selectedOption.toLowerCase()) + ); + }, + + _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/pages/query-detail.less b/website/assets/styles/pages/query-detail.less index 2a7caac610..0f9dc087b7 100644 --- a/website/assets/styles/pages/query-detail.less +++ b/website/assets/styles/pages/query-detail.less @@ -6,8 +6,15 @@ } h6 { + font-family: 'Nunito'; font-size: 16px; - line-height: 24px; + line-height: 25px; + } + + p { + &.platform, &.purpose { + margin-bottom: 8px; + } } a { @@ -15,6 +22,13 @@ color: @core-vibrant-blue; } + img { + &.logo { + height: 24px; + width: 24px; + } + } + .query-tip { background-color: @ui-off-white; @@ -32,4 +46,56 @@ } + .avatar-frame { + width: 24px; + height: 24px; + position: relative; + overflow: hidden; + border-radius: 50%; + &:hover { + cursor: pointer; + } + + img { + display: inline; + margin: 0 auto; + height: 100%; + width: auto; + } + } + + .divider { + border-bottom: 1px solid; + border-color: #E2E4EA; + } + + .platforms, .purpose, .contributors, .contribute { + min-height: 36px; + p, a { + font-family: 'Nunito'; + font-size: 16px; + line-height: 24px; + } + } + + .remediation { + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; + p { + font-family: 'Nunito'; + font-size: 16px; + line-height: 24px; + } + } + + @media (max-width: 575px) { + + h2 { + font-size: 28px; + line-height: 36px; + } + + } + } diff --git a/website/assets/styles/pages/query-library.less b/website/assets/styles/pages/query-library.less index b503c60ef3..ba8d336d74 100644 --- a/website/assets/styles/pages/query-library.less +++ b/website/assets/styles/pages/query-library.less @@ -22,18 +22,85 @@ color: @core-vibrant-blue; } - select { - color: @core-vibrant-blue; - border: none; + img { + &.logo { + height: 16px; + width: 16px; + } } input { - max-width: 176px; + height: 54px; + width: 250px; + border: 1px solid #C5C7D1; + border-radius: 8px; + padding: 15px; + &.mobile { + width: 100%; + margin-right: 0; + margin-left: 0; + } + } + + select { + color: @core-vibrant-blue; + border: 0px; + &:focus { + border: 0px; + } + &.select-purpose { + width: 102px; + } + &.select-platform { + width: 118px; + } + &.mobile { + height: 50px; + width: 100%; + margin-right: 0; + margin-left: 0; + border-radius: 8px; + padding: 12px; + } + } + + .library { + max-width: 860px; + } + + .description { + padding: 0px 30px 0px 30px; + p { + font-size: 16px; + line-height: 25px; + } + } + + .select-mobile-border { + height: 54px; + width: 100%; + margin-right: 0; + margin-left: 0; + border: 1px solid #C5C7D1; + border-radius: 8px; + padding-right: 15px; +} + + .select-mobile { + padding-left: 30px; + padding-right: 30px; + padding-bottom: 12px; + } + + .search-mobile { + padding-left: 30px; + padding-right: 30px; + padding-bottom: 12px; } .filter-and-search-bar { - margin-left: 30px; - margin-right: 30px; + padding-left: 45px; + padding-right: 45px; } .contributors, .platforms { @@ -42,7 +109,6 @@ font-size: 13px; line-height: 20px; } - } .row { @@ -50,12 +116,17 @@ align-items: center; } + .divider { + margin-left: 30px; + margin-right: 30px; + border-bottom: 1px solid; + border-color: #E2E4EA; + } + .card.results { box-shadow: none; border: none; border-radius: 8px; - border-bottom: 1px solid; - border-color: #E2E4EA; &:hover { background-color: #F1F0FF; @@ -69,11 +140,48 @@ border-color: @ui-off-white; box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1); margin-bottom: 90px; + width: 100%; } .card-body { padding: 30px; + padding-top: 24px; + padding-bottom: 24px; + } + + .avatar-frame { + width: 21px; + height: 21px; + position: relative; + overflow: hidden; + border-radius: 50%; + &:hover { + cursor: pointer; + } + + img { + display: inline; + margin: 0 auto; + height: 100%; + width: auto; + } + } + + @media (max-width: 575px) { + + h2 { + font-size: 28px; + line-height: 36px; + } + + .contributors, .platforms { + + p { + font-size: 13px; + line-height: 20px; + } + } } } diff --git a/website/views/pages/query-detail.ejs b/website/views/pages/query-detail.ejs index c5bf2e9e80..6f4c3a2f76 100644 --- a/website/views/pages/query-detail.ejs +++ b/website/views/pages/query-detail.ejs @@ -1,9 +1,9 @@
-
+

{{query.name}}

-
{{query.description}}
+
{{query.description}}
lightbulb

{{query.tip}}

@@ -11,22 +11,19 @@

Query

{{query.query}} -
+

Remediation

-
    -
  • {{query.remediation}}
  • -
+

{{query.remediation}}

-
Platforms

- - - + + +

@@ -35,15 +32,68 @@

{{query.purpose}}

-
+
Contributors
- -

{{query.contributors}}

+
+
+
+ GitHub profile image +
+
+
Contribute to this page
View source
- +
+

{{query.name}}

+
{{query.description}}
+
+
+
+
+
+
Platforms
+

+ + + +

+
+
+
+
Purpose
+

{{query.purpose}}

+
+
+
+
+
+
Contributors
+
+
+
+ GitHub profile image +
+
+
+
+
+
+
Contribute to this page
+ View source +
+
+
+
+
+

Query

+ {{query.query}} +
+

Remediation

+

{{!query.remediation ? "N/A" : query.remediation}}

+
+
<%- /* Expose server-rendered data as window.SAILS_LOCALS :: */ exposeLocalsToBrowser() %> diff --git a/website/views/pages/query-library.ejs b/website/views/pages/query-library.ejs index 9a96b8651e..16b758ec8c 100644 --- a/website/views/pages/query-library.ejs +++ b/website/views/pages/query-library.ejs @@ -1,24 +1,49 @@
-
+

Standard query library

-
Fleet's standard query library includes a growing collection of useful queries for organizations deploying Fleet and osquery.
-