diff --git a/services/artifactory/artifactory.service.js b/services/artifactory/artifactory.service.js
new file mode 100644
index 0000000000000..654e4ddc991e1
--- /dev/null
+++ b/services/artifactory/artifactory.service.js
@@ -0,0 +1,211 @@
+import Joi from 'joi'
+import { optionalUrl } from '../validators.js'
+import { BaseService } from '../index.js'
+import { renderVersionBadge } from '../version.js'
+import { getCachedResource } from '../../core/base-service/resource-cache.js'
+
+const ARTIFACTORY_LATEST_VERSION_ENDPOINT = 'api/search/latestVersion'
+const ONE_HOUR = 1 * 3600 * 1000
+
+const queryParamSchema = Joi.object({
+ // auth related optional properties
+ server: optionalUrl.default(''),
+ token: Joi.string().default(''),
+ username: Joi.string().default(''),
+ password: Joi.string().default(''),
+
+ // api related optional properties
+ repos: Joi.string().default(''),
+ remote: Joi.string().default(''),
+ version: Joi.string().default(''),
+}).required()
+
+const documentation = `
+
+ Query for the last released version of an artifact using the Latest Version endpoint.
+
+
+ This service will cache results for 1 hour so as not to overload a given Artifactory instance with potentially long running query operations.
+
+
+ Authentication is done either through basic auth or using a bearer token. While the options exist to pass along credentials as query params, users
+ are strongly encouraged to instead set the relevant auth related environment variables server side which this service will pick up and read at runtime.
+
+
+ The following environment variables are supported:
+
+ - ARTIFACTORY_URL
+ - ARTIFACTORY_TOKEN
+ - ARTIFACTORY_USERNAME
+ - ARTIFACTORY_PASSWORD
+
+ If using a Token for authentication you do not need to set the username and password env-vars.
+
+
+ For more technical information on the available properties the Latest Version endpoint uses, and their potential drawbacks and limitations, please consult the following developer documentation here.
+
+ `
+
+export default class Artifactory extends BaseService {
+ static category = 'version'
+ static route = {
+ base: 'artifactory/v',
+ pattern: ':group/:artifact?',
+ queryParamSchema,
+ }
+
+ static examples = [
+ {
+ title: 'Artifactory Latest Version',
+ documentation,
+ namedParams: {
+ group: 'org.jfrog.artifactory.client',
+ artifact: 'artifactory-java-client-services',
+ },
+ queryParams: {
+ repos: 'libs-release-local',
+ remote: '1',
+ version: '1.0-SNAPSHOT',
+ server: 'https://my.artifactory.server/artifactory',
+ token: 'OTEfj876SDF2345JKIB/H&+FFGH',
+ username: 'jerry',
+ password: 'seinfeld',
+ },
+ staticPreview: Artifactory.render('2.1.0'),
+ },
+ ]
+
+ static defaultBadgeData = { label: 'artifactory' }
+
+ static render(version) {
+ return renderVersionBadge({ version })
+ }
+
+ static orElseEnvVar(possibleValue, envVarName) {
+ if (possibleValue !== '') {
+ return possibleValue
+ } else {
+ const envValue = process.env[envVarName]
+ if (envValue && envValue !== 'undefined') {
+ return envValue
+ } else {
+ return ''
+ }
+ }
+ }
+
+ static getAuthorizationHeaderValue(
+ possibleToken,
+ possibleUsername,
+ possiblePassword,
+ ) {
+ // check for token value first
+ const artToken = Artifactory.orElseEnvVar(
+ possibleToken,
+ 'ARTIFACTORY_TOKEN',
+ )
+ if (artToken !== '') {
+ return `Bearer ${artToken}`
+ }
+
+ // if no token then check for username and password
+ const artUsername = Artifactory.orElseEnvVar(
+ possibleUsername,
+ 'ARTIFACTORY_USERNAME',
+ )
+ if (artUsername !== '') {
+ const artPassword = Artifactory.orElseEnvVar(
+ possiblePassword,
+ 'ARTIFACTORY_PASSWORD',
+ )
+ if (artUsername !== '') {
+ return `Basic ${btoa(`${artUsername}:${artPassword}`)}`
+ }
+ }
+
+ return ''
+ }
+
+ async fetchVersion({ requestParams }) {
+ // cache results so as not to overload
+ const result = await getCachedResource({
+ url: requestParams.artifactoryEndpoint,
+ ttl: ONE_HOUR,
+ json: false,
+ scraper: response => response,
+ options: requestParams.options,
+ })
+
+ // Request path here does not use caching but does
+ // allow us to catch specific errors and return
+ // more specific responses: can we do something
+ // similar when using the cachedResource function?
+ //
+ // const { res, buffer } = await this._request({
+ // url: requestParams.artifactoryEndpoint,
+ // options: requestParams.options,
+ // httpErrors: { 404: 'Not Found', 401: 'Bad Credentials' },
+ // })
+
+ return result
+ }
+
+ async handle(
+ { group, artifact },
+ { server, token, username, password, repos, remote, version },
+ ) {
+ // Artifactory can return plain/text or json for this endpoint we're hitting so
+ // we need to effectively accept all as we're not sure what we're going to be returned
+ const defaultHeaders = { Accept: '*/*' }
+
+ // dump query params to debug console
+ console.debug(`***** artifactory found the following query properties *****
+
+ group=${group}
+ artifact=${artifact}
+ server=${server}
+ token=${token}
+ username=${username}
+ password=${password}
+ repos=${repos}
+ remote=${remote}
+ version=${version}
+ `)
+
+ // set and check Artifactory URL
+ const artifactoryUrl = Artifactory.orElseEnvVar(server, 'ARTIFACTORY_URL')
+ if (artifactoryUrl === '') {
+ const msg = 'No Artifactory URL found'
+ return this.constructor.render({ msg })
+ }
+ const artifactoryEndpoint = `${artifactoryUrl}/${ARTIFACTORY_LATEST_VERSION_ENDPOINT}`
+
+ // set and check authorization values
+ const authValue = Artifactory.getAuthorizationHeaderValue(
+ token,
+ username,
+ password,
+ )
+ if (authValue) {
+ defaultHeaders.Authorization = authValue
+ }
+
+ // set our request parameters to query Artifactory
+ const requestParams = {
+ artifactoryEndpoint,
+ options: {
+ headers: defaultHeaders,
+ searchParams: {
+ g: group,
+ a: artifact,
+ v: version,
+ remote,
+ repos,
+ },
+ },
+ }
+
+ const foundVersion = await this.fetchVersion({ requestParams })
+ return Artifactory.render(foundVersion)
+ }
+}