DEVELOPERS BLOG

Cylance REST API: Locking down devices remotely

Are you looking for help integrating the BlackBerry Cylance REST API in your applications? Look no further; this blog post goes over everything you need to know to get started. In this blog, you will learn how to get authentication tokens, device IDs, convert different types of device IDs, and how to remotely lockdown devices for a specified amount of time.

The code samples in this blog post will be using the BlackBerry Cylance REST API with JavaScript in a React Native application, but the underlying principles are the same regardless of the language.

The code snippets below are referencing a sample located here on our GitHub page.

Authentication and authorization

Prior to making any API request, you must first generate a valid JSON Web Token (JWT) to receive an access token from the /auth/ API. You can then use the access token to authenticate all other REST requests.

You can learn more about JWT’s here, and view their ‘Request For Comments’ (RFC) standard here.

As mentioned above, you must generate a JWT encoded value to use as a parameter to request an access token. This access token can be used to make further requests. We have done this using React with Redux which has a lot of boilerplate code in order to work, so I’ll only be showing the parts that really show how to generate the tokens.

This method generates the JWT encoded value:

cylance-apis.js



generateJWTEncoded(app_id, app_secret, tenent_id) { const jti_value = uuidv4(); // using milliseconds here because react-native-pure-jwt converts the naturally // javascript epoch scale of milliseconds to the seconds scale commonly found in // JWT(s). Note that the (Name of API?) API only accepts Epoch Seconds, but this // react-native-pure-jwt library handles the milliseconds -> seconds conversion // for us implicitly. const timeout = 1800*1000; const now = Date.now(); const exp = now + timeout; const claims = { iat: now, sub: app_id, tid: tenent_id, jti: jti_value, iss: 'http://cylance.com', exp: exp, }; const algorithm = { alg: 'HS256', }; return jwt.sign(claims, app_secret, algorithm).then((encoded) => { return encoded; }); }

For reference, the JWT object comes from the ‘react-native-pure-jwt’ package in NPM here. This specific package is used over others because of its support for the `jti_value` in the token. A JTI value is required for authentication. This value is outlined in the JWT RFC[MS1] , however, it is not supported in many mainstream packages. Keep an eye out for this value when selecting a JWT library for other platforms.

In the claims dictionary, iat is the time when the token was issued in epoch time seconds, sub is the Application ID, tid is the Tenant ID, jti is a Unique ID for the token, iss is the principal issuing the token, exp is the time when the token expires.

Note that we used milliseconds here for the iat and exp claims. This is only because the ‘react-native-pure-jwt’ library automatically converts that to seconds since JWTs often use epoch seconds but JavaScript naturally uses epoch milliseconds.

You can access your Application ID, Tenant ID, and App Secret in the BlackBerry PROTECT console in Settings, under “Integrations”.

Once you have the JWT, you can use it to get an access token, as shown in the following method.

cylance-apis.js



getTokenFromAPI(serviceEndpointURL, JWTEncoded) { const authURL = serviceEndpointURL.concat('auth/v2/token'); const payload = { auth_token: JWTEncoded, }; const options = { headers: {'Content-Type': 'application/json; charset=utf-8'}, }; return axios .post(authURL, payload, options) .then((response) => { return response; }) .catch((error) => { throw error; }); }

Here we make a POST request to the following endpoint: {{service_endpoint}}/auth/v2/token

We pass the JWT we generated in the previous step in the payload of the request. The service endpoint is the base URL which identifies the region which your servers belong to.

In this method, we return the axios response. Then we check the response to see if it succeeded and parse the JSON for the access token. This is done in the following code block.

actions/token/token.js



CylanceAPI.generateJWTEncoded(app_id, app_secret, tenent_id).then( (JWTEncoded) => { CylanceAPI.getTokenFromAPI(selected_service_endpoint, JWTEncoded) .then((response) => { if (response.status === 200) { var access_token = response.data.access_token; dispatch({ type: GET_TOKEN_SUCCESS, payload: { token: access_token, endpoint: selected_service_endpoint, }, }); } else { dispatch({ type: GET_TOKEN_ERROR, payload: 'Token Endpoint threw non 200', }); } }) .catch((error) => { dispatch({ type: GET_TOKEN_ERROR, payload: error.response.data.message, }); }); }, );

Here, we check the status code of the response, if it’s good we get the token and dispatch a GET_TOKEN_SUCCESS action Otherwise, we dispatch a GET_TOKEN_ERROR action.

Now let’s go over a couple other pre-requisites before we start sending off some more interesting requests.

Getting Device IDs

For device specific requests, you need to specify which device you are referring to and you can do that by using the device IDs. But how do you get those device IDs? Well, good news, there’s an endpoint for that! And here’s how it’s used.

cylance-apis.js



getDevices(serviceEndpointURL, token, page, page_size) { const pagination = `?page=${page}&page_size=${page_size}`; const endpoint = serviceEndpointURL.concat(`devices/v2${pagination}`); const options = { headers: { 'Content-Type': 'application/json; charset=utf-8', Authorization: 'Bearer ' + token, }, }; return axios .get(endpoint, options) .then((response) => { return response; }) .catch((error) => { throw error; }); }

Here, we make a GET request to the following endpoint: {{service_endpoint}}/devices/v2?page=m&page_size=n

 

This API call returns the devices on that page in a standard known as pagination. For example, if you wanted to get the first 100 devices, you would set the page parameter to 1 and the page_size parameter to 100 (the default values are 1 and 10 respectively).

Of course, you need to authorize yourself with the access token passed in the request header.

Once all this is done, you will receive a response that looks like the following:



{ "page_number": "1", "page_size": "10", "total_pages": "1", "total_number_of_items": "1", "page_items": [ { "id": "e378dacb-9324-453a-b8c6-5a8406952195", "name": "User-Laptop-A123", "state": "Offline", "agent_version": "2.0.1530", "policy": { "id": "d5c6d6a3-0599-4fb5-96bc-0fdc7eacb6ea", "name": "Test Policy" }, "date_first_registered": "2019-04-26T19:55:33", "ip_addresses": [ "123.45.67.89" ], "mac_addresses": [ "00-00-00-00-00-00" ] } ] }

Subtleties with Device IDs

It is important for you to know is the difference between CylanceOPTICS device IDs and CylancePROTECT device IDs. The difference can be subtle and using the wrong one without realizing it can cause you to bang your head against the desk trying to debug your code for hours. To make matters worse, using the wrong format of ID may give you an HTTP 200 status, but the response won’t be correct. To avoid this issue, be sure to read the documentation of the API here to ensure you are using the correct format for any given request.

CylanceOPTICS device IDs are all upper-case and have no dashes.

Eg: 45E07F34E76B4A9EB167D6D0C510D6BA

CylancePROTECT device IDs are all lower-case and have dashes.

Eg: 45e07f34-e76b-4a9e-b167-d6d0c510d6ba

The interesting thing is that the OPTICS and PROTECT IDs for the same device will have the same alphanumeric ID, the only difference is the case and the hyphens.

Much of the time you will query and receive the device ID from the CylancePROTECT API by using the devices endpoint mentioned in the previous section. This ID will be in the PROTECT format, but you will need to convert it to the OPTICS format to make the required request. This can be done easily enough by simply removing all the dashes and converting the lower-case letters to upper-case.

Locking down devices

Now that we have the pre-requisites out of the way, we can get to the fun stuff! Making an actual request to lockdown devices. This request requires us to specify the device we want to lock down and the amount of time the device will be locked down. The minimum amount of time is 5 minutes, and the maximum amount of time is 3 days.

The way that this app does this is by giving the user the option to lock down the device whenever a threat has been detected on a remote device. I won’t be going into all the code needed to render the screen, passing down the props, etc. since that stuff can be found in React Native tutorials. Instead, I’ll only be showing the parts required to actually send off the request.

I’ve added an endpoint to make the required request:

cylance-apis.js



lockdownDevice(serviceEndpointURL, token, device_id, days, hours, mins) { const timeOption = `?value=true&expires=${days}:${hours}:${mins}`; const endpoint = serviceEndpointURL.concat(`devicecommands/v2/ ${convertProtectIDToOpticsID(device_id)}/lockdown${timeOption}`); const options = { headers: { Accept: 'application/json', Authorization: 'Bearer ' + token, } }; return axios .put(endpoint, {}, options) .then((response) => { return response; }) .catch((error) => { throw error; }); }

This is a standard request, similar to the request to get the access token or the devices earlier in this blog post. There are a couple things to note here. For one, this is a PUT request, not a POST request. Also, we have to use the OPTICS ID so I’ve used a little helper function to convert from the PROTECT ID to the OPTICS ID. Finally, the request must take the following form: {{service_endpoint}}/devicecommands/v2/{{device_id}}/lockdown?value=true&expires=d:hh:mm

Then we can use this method along with the token and device ID in our lockdown action to send the lockdown request.

actions/lockdown/lockdown.js



export const lockdownDevice = (device_id, days, hours, mins) => (dispatch, getState) => { dispatch({type: LOCKDOWN_REQUEST}); const token = getState().token.token; const endpoint = getState().token.endpoint; CylanceAPI.lockdownDevice(endpoint, token, device_id, days, hours, mins) .then((response) => { if (response.status === 200 || response.status === 201) { dispatch({type: LOCKDOWN_SUCCESS, payload: response.data}); } else { throw response.status; } }) .catch((error) => { dispatch({ type: LOCKDOWN_ERROR, payload: StatusCodes[error.response.status], }); }); };

In the code sample above, we get the token and service endpoint from the React Native store. The token was populated earlier when we went through the authentication portion. Finally, we call the function with valid parameters and the selected device will be locked down!

And that’s it! Everything you need to know to be able to make Cylance API requests. Hope you learned something from this blog.  Good luck with sending your own API requests!

Nathaniel Johnston

About Nathaniel Johnston

Enterprise Solutions Development Student - IoT